include/vcl/pdfextoutdevdata.hxx | 5 sc/qa/extras/scpdfexport.cxx | 96 +++++++++ sc/qa/extras/testdocuments/tdf123870.ods |binary sc/source/ui/inc/output.hxx | 15 + sc/source/ui/unoobj/docuno.cxx | 305 +++++++++++++++++++------------ sc/source/ui/view/output.cxx | 76 +++++++ sc/source/ui/view/output2.cxx | 68 ++++++ sc/source/ui/view/printfun.cxx | 51 +++++ 8 files changed, 495 insertions(+), 121 deletions(-)
New commits: commit b3c93b16d62e809500005edc749af4b8ad10162c Author: Tibor Nagy <tibor.nagy.ext...@allotropia.de> AuthorDate: Wed Jan 3 11:18:19 2024 +0100 Commit: Thorsten Behrens <thorsten.behr...@allotropia.de> CommitDate: Mon Jan 8 21:24:49 2024 +0100 tdf#123870 sc: fix tagged content for accessible PDF export Change-Id: Iec7ea2d5acb66d7c5f9241285cf98f83e02c2445 Reviewed-on: https://gerrit.libreoffice.org/c/core/+/161581 Tested-by: Jenkins Reviewed-by: Thorsten Behrens <thorsten.behr...@allotropia.de> diff --git a/include/vcl/pdfextoutdevdata.hxx b/include/vcl/pdfextoutdevdata.hxx index 206dfa4adc97..670ff234e2bd 100644 --- a/include/vcl/pdfextoutdevdata.hxx +++ b/include/vcl/pdfextoutdevdata.hxx @@ -32,6 +32,7 @@ class Graphic; class GDIMetaFile; class SdrObject; struct SwEnhancedPDFState; +struct ScEnhancedPDFState; namespace vcl { @@ -98,6 +99,7 @@ class VCL_DLLPUBLIC PDFExtOutDevData final : public ExtOutDevData ::std::map<SdrObject const*, ::std::vector<sal_Int32>> m_ScreenAnnotations; SwEnhancedPDFState * m_pSwPDFState = nullptr; + ScEnhancedPDFState * m_pScPDFState = nullptr; public: @@ -159,6 +161,9 @@ public: SwEnhancedPDFState * GetSwPDFState() { return m_pSwPDFState; } void SetSwPDFState(SwEnhancedPDFState *const pSwPDFState) { m_pSwPDFState = pSwPDFState; } + ScEnhancedPDFState* GetScPDFState() { return m_pScPDFState; } + void SetScPDFState(ScEnhancedPDFState* const pScPDFState) { m_pScPDFState = pScPDFState; } + const Graphic& GetCurrentGraphic() const; /** Start a new group of render output diff --git a/sc/qa/extras/scpdfexport.cxx b/sc/qa/extras/scpdfexport.cxx index bcca563ec9b3..bdb4eb3e44a0 100644 --- a/sc/qa/extras/scpdfexport.cxx +++ b/sc/qa/extras/scpdfexport.cxx @@ -34,6 +34,9 @@ #if USE_TLS_NSS #include <nss.h> #endif +#include <vcl/filter/pdfdocument.hxx> +#include <tools/zcodec.hxx> +#include <o3tl/string_view.hxx> using namespace css::lang; using namespace ::com::sun::star; @@ -62,6 +65,7 @@ public: void testExportFitToPage_Tdf103516(); void testUnoCommands_Tdf120161(); void testTdf64703_hiddenPageBreak(); + void testTdf123870(); void testTdf143978(); void testTdf120190(); void testTdf84012(); @@ -73,6 +77,7 @@ public: CPPUNIT_TEST(testExportFitToPage_Tdf103516); CPPUNIT_TEST(testUnoCommands_Tdf120161); CPPUNIT_TEST(testTdf64703_hiddenPageBreak); + CPPUNIT_TEST(testTdf123870); CPPUNIT_TEST(testTdf143978); CPPUNIT_TEST(testTdf120190); CPPUNIT_TEST(testTdf84012); @@ -149,7 +154,8 @@ void ScPDFExportTest::exportToPDF(const uno::Reference<frame::XModel>& xModel, c css::uno::Sequence<css::beans::PropertyValue> aFilterData{ comphelper::makePropertyValue("Selection", xCellRange), comphelper::makePropertyValue("Printing", sal_Int32(2)), - comphelper::makePropertyValue("ViewPDFAfterExport", true) + comphelper::makePropertyValue("ViewPDFAfterExport", true), + comphelper::makePropertyValue("PDFUACompliance", true) }; // init set of params for storeToURL() call @@ -388,6 +394,94 @@ void ScPDFExportTest::testTdf64703_hiddenPageBreak() } } +void ScPDFExportTest::testTdf123870() +{ + loadFromURL(u"tdf123870.ods"); + uno::Reference<frame::XModel> xModel(mxComponent, uno::UNO_QUERY); + + // A1:G4 + ScRange range1(0, 0, 0, 6, 4, 0); + exportToPDF(xModel, range1); + + vcl::filter::PDFDocument aDocument; + SvFileStream aStream(maTempFile.GetURL(), StreamMode::READ); + CPPUNIT_ASSERT(aDocument.Read(aStream)); + + // The document has one page. + std::vector<vcl::filter::PDFObjectElement*> aPages = aDocument.GetPages(); + CPPUNIT_ASSERT_EQUAL(static_cast<size_t>(1), aPages.size()); + + vcl::filter::PDFObjectElement* pContents = aPages[0]->LookupObject("Contents"_ostr); + CPPUNIT_ASSERT(pContents); + vcl::filter::PDFStreamElement* pStream = pContents->GetStream(); + CPPUNIT_ASSERT(pStream); + SvMemoryStream& rObjectStream = pStream->GetMemory(); + // Uncompress it. + SvMemoryStream aUncompressed; + ZCodec aZCodec; + aZCodec.BeginCompression(); + rObjectStream.Seek(0); + aZCodec.Decompress(rObjectStream, aUncompressed); + CPPUNIT_ASSERT(aZCodec.EndCompression()); + + auto pStart = static_cast<const char*>(aUncompressed.GetData()); + const char* const pEnd = pStart + aUncompressed.GetSize(); + + enum + { + Default, + Artifact, + Tagged + } state + = Default; + + auto nLine(0); + auto nTagged(0); + auto nArtifacts(0); + while (true) + { + ++nLine; + auto const pLine = ::std::find(pStart, pEnd, ' '); + if (pLine == pEnd) + { + break; + } + std::string_view const line(pStart, pLine - pStart); + pStart = pLine + 1; + if (!line.empty() && line[0] != '%') + { + ::std::cerr << nLine << ": " << line << " "; + if (o3tl::ends_with(line, "/Artifact BMC")) + { + CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state); + state = Artifact; + ++nArtifacts; + } + else if ((o3tl::starts_with(line, "/P<</MCID") && o3tl::ends_with(line, ">>BDC")) + || (o3tl::starts_with(line, "/Figure<</MCID") + && o3tl::ends_with(line, ">>BDC"))) + { + CPPUNIT_ASSERT_EQUAL_MESSAGE("unexpected nesting", Default, state); + state = Tagged; + ++nTagged; + } + else if (line == "EMC") + { + CPPUNIT_ASSERT_MESSAGE("unexpected end", state != Default); + state = Default; + } + else if (nLine > 1) // first line is expected "0.1 w" + { + CPPUNIT_ASSERT_MESSAGE("unexpected content outside MCS", state != Default); + } + } + } + // text in cell + 1 shape + CPPUNIT_ASSERT_EQUAL(static_cast<decltype(nTagged)>(9), nTagged); + // header, footer, background color, color scale, shadow, cell border + CPPUNIT_ASSERT(nArtifacts >= 6); +} + void ScPDFExportTest::testTdf143978() { std::shared_ptr<vcl::pdf::PDFium> pPDFium = vcl::pdf::PDFiumLibrary::get(); diff --git a/sc/qa/extras/testdocuments/tdf123870.ods b/sc/qa/extras/testdocuments/tdf123870.ods new file mode 100644 index 000000000000..bccf80662509 Binary files /dev/null and b/sc/qa/extras/testdocuments/tdf123870.ods differ diff --git a/sc/source/ui/inc/output.hxx b/sc/source/ui/inc/output.hxx index e4763767b7f5..b94fbb1ec7ac 100644 --- a/sc/source/ui/inc/output.hxx +++ b/sc/source/ui/inc/output.hxx @@ -26,6 +26,7 @@ #include <tools/gen.hxx> #include <editeng/svxenum.hxx> #include <vcl/outdev.hxx> +#include <vcl/pdfwriter.hxx> #include <tools/degree.hxx> #include <o3tl/deleter.hxx> #include <optional> @@ -61,6 +62,17 @@ enum ScOutputType { OUTTYPE_WINDOW, OUTTYPE_PRINTER }; class ClearableClipRegion; typedef std::unique_ptr<ClearableClipRegion, o3tl::default_delete<ClearableClipRegion>> ClearableClipRegionPtr; +typedef std::map<SCROW, sal_Int32> TableRowIdMap; +typedef std::map<std::pair<SCROW, SCCOL>, sal_Int32> TableDataIdMap; +struct ScEnhancedPDFState +{ + sal_Int32 m_WorksheetId = -1; + sal_Int32 m_TableId = -1; + TableRowIdMap m_TableRowMap; + TableDataIdMap m_TableDataMap; + ScEnhancedPDFState(){}; +}; + /// Describes reference mark to be drawn, position & size in TWIPs struct ReferenceMark { tools::Long nX; @@ -342,6 +354,9 @@ public: void SetSnapPixel(); + bool ReopenPDFStructureElement(vcl::PDFWriter::StructElement aType, SCROW nRow = -1, + SCCOL nCol = -1); + void DrawGrid(vcl::RenderContext& rRenderContext, bool bGrid, bool bPage, bool bMergeCover = false); void DrawStrings( bool bPixelToLogic = false ); diff --git a/sc/source/ui/unoobj/docuno.cxx b/sc/source/ui/unoobj/docuno.cxx index e1a70b742517..747600e9af63 100644 --- a/sc/source/ui/unoobj/docuno.cxx +++ b/sc/source/ui/unoobj/docuno.cxx @@ -131,6 +131,7 @@ #include <table.hxx> #include <appoptio.hxx> #include <formulaopt.hxx> +#include <output.hxx> #include <strings.hrc> @@ -2111,6 +2112,156 @@ uno::Sequence<beans::PropertyValue> SAL_CALL ScModelObj::getRenderer( sal_Int32 return aSequence; } +static void lcl_PDFExportHelper(const OutputDevice* pDev, const OUString& rTabName, bool bIsFirstPage) +{ + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + if (pPDF) + { + css::lang::Locale const docLocale(Application::GetSettings().GetLanguageTag().getLocale()); + pPDF->SetDocumentLocale(docLocale); + + // first page of a sheet: add outline item for the sheet name + + if (pPDF->GetIsExportBookmarks()) + { + // the sheet starts at the top of the page + tools::Rectangle aArea(pDev->PixelToLogic(tools::Rectangle(0, 0, 0, 0))); + sal_Int32 nDestID = pPDF->CreateDest(aArea); + // top-level + pPDF->CreateOutlineItem(-1/*nParent*/, rTabName, nDestID); + } + // #i56629# add the named destination stuff + if (pPDF->GetIsExportNamedDestinations()) + { + tools::Rectangle aArea(pDev->PixelToLogic(tools::Rectangle(0, 0, 0, 0))); + //need the PDF page number here + pPDF->CreateNamedDest(rTabName, aArea); + } + + if (pPDF->GetIsExportTaggedPDF()) + { + if (bIsFirstPage) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::Document, "Workbook"); + else + { // if there is a new worksheet(not first), delete and add new ScPDFState + assert(pPDF->GetScPDFState()); + delete pPDF->GetScPDFState(); + pPDF->SetScPDFState(nullptr); + } + + assert(pPDF->GetScPDFState() == nullptr); + pPDF->SetScPDFState(new ScEnhancedPDFState()); + } + } +} + +static void lcl_PDFExportBookmarkHelper(OutputDevice* pDev, ScDocument& rDoc, + const std::unique_ptr<ScPrintFuncCache>& pPrintFuncCache, + const ScMarkData& rMark, sal_Int32 nTab) +{ + // resolve the hyperlinks for PDF export + + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + if (!pPDF || pPDF->GetBookmarks().empty()) + return; + + // iterate over the hyperlinks that were output for this page + + std::vector<vcl::PDFExtOutDevBookmarkEntry>& rBookmarks = pPDF->GetBookmarks(); + for (const auto& rBookmark : rBookmarks) + { + OUString aBookmark = rBookmark.aBookmark; + if (aBookmark.toChar() == '#') + { + // try to resolve internal link + + OUString aTarget(aBookmark.copy(1)); + + ScRange aTargetRange; + tools::Rectangle aTargetRect; // 1/100th mm + bool bIsSheet = false; + bool bValid = lcl_ParseTarget(aTarget, aTargetRange, aTargetRect, bIsSheet, rDoc, nTab); + + if (bValid) + { + sal_Int32 nPage = -1; + tools::Rectangle aArea; + if (bIsSheet) + { + // Get first page for sheet (if nothing from that sheet is printed, + // this page can show a different sheet) + nPage = pPrintFuncCache->GetTabStart(aTargetRange.aStart.Tab()); + aArea = pDev->PixelToLogic(tools::Rectangle(0, 0, 0, 0)); + } + else + { + pPrintFuncCache->InitLocations(rMark, pDev); // does nothing if already initialized + + ScPrintPageLocation aLocation; + if (pPrintFuncCache->FindLocation(aTargetRange.aStart, aLocation)) + { + nPage = aLocation.nPage; + + // get the rectangle of the page's cell range in 1/100th mm + ScRange aLocRange = aLocation.aCellRange; + tools::Rectangle aLocationMM = rDoc.GetMMRect( + aLocRange.aStart.Col(), aLocRange.aStart.Row(), aLocRange.aEnd.Col(), + aLocRange.aEnd.Row(), aLocRange.aStart.Tab()); + tools::Rectangle aLocationPixel = aLocation.aRectangle; + + // Scale and move the target rectangle from aLocationMM to aLocationPixel, + // to get the target rectangle in pixels. + assert(aLocationPixel.GetWidth() != 0 && aLocationPixel.GetHeight() != 0); + + Fraction aScaleX(aLocationPixel.GetWidth(), aLocationMM.GetWidth()); + Fraction aScaleY(aLocationPixel.GetHeight(), aLocationMM.GetHeight()); + + tools::Long nX1 + = aLocationPixel.Left() + + static_cast<tools::Long>( + Fraction(aTargetRect.Left() - aLocationMM.Left(), 1) * aScaleX); + tools::Long nX2 + = aLocationPixel.Left() + + static_cast<tools::Long>( + Fraction(aTargetRect.Right() - aLocationMM.Left(), 1) * aScaleX); + tools::Long nY1 + = aLocationPixel.Top() + + static_cast<tools::Long>( + Fraction(aTargetRect.Top() - aLocationMM.Top(), 1) * aScaleY); + tools::Long nY2 + = aLocationPixel.Top() + + static_cast<tools::Long>( + Fraction(aTargetRect.Bottom() - aLocationMM.Top(), 1) * aScaleY); + + if (nX1 > aLocationPixel.Right()) + nX1 = aLocationPixel.Right(); + if (nX2 > aLocationPixel.Right()) + nX2 = aLocationPixel.Right(); + if (nY1 > aLocationPixel.Bottom()) + nY1 = aLocationPixel.Bottom(); + if (nY2 > aLocationPixel.Bottom()) + nY2 = aLocationPixel.Bottom(); + + // The link target area is interpreted using the device's MapMode at + // the time of the CreateDest call, so PixelToLogic can be used here, + // regardless of the MapMode that is actually selected. + aArea = pDev->PixelToLogic(tools::Rectangle(nX1, nY1, nX2, nY2)); + } + } + + if (nPage >= 0) + pPDF->SetLinkDest(rBookmark.nLinkId, pPDF->CreateDest(aArea, nPage)); + } + } + else + { + // external link, use as-is + pPDF->SetLinkURL(rBookmark.nLinkId, aBookmark); + } + } + rBookmarks.clear(); +} + void SAL_CALL ScModelObj::render( sal_Int32 nSelRenderer, const uno::Any& aSelection, const uno::Sequence<beans::PropertyValue>& rOptions ) { @@ -2126,6 +2277,8 @@ void SAL_CALL ScModelObj::render( sal_Int32 nSelRenderer, const uno::Any& aSelec OUString aPagesStr; bool bRenderToGraphic = false; bool bSinglePageSheets = false; + bool bIsFirstPage = false; + bool bIsLastPage = false; if ( !FillRenderMarkData( aSelection, rOptions, aMark, aStatus, aPagesStr, bRenderToGraphic ) ) throw lang::IllegalArgumentException(); @@ -2140,7 +2293,14 @@ void SAL_CALL ScModelObj::render( sal_Int32 nSelRenderer, const uno::Any& aSelec if ( rValue.Name == "SinglePageSheets" ) { rValue.Value >>= bSinglePageSheets; - break; + } + else if (rValue.Name == "IsFirstPage") + { + rValue.Value >>= bIsFirstPage; + } + else if (rValue.Name == "IsLastPage") + { + rValue.Value >>= bIsLastPage; } } @@ -2198,8 +2358,23 @@ void SAL_CALL ScModelObj::render( sal_Int32 nSelRenderer, const uno::Any& aSelec rDoc.SetVisibleTab(nVisTab); } + OUString aTabName; + rDoc.GetName(nVisTab, aTabName); + lcl_PDFExportHelper(pDev, aTabName, bIsFirstPage); + pDocShell->DoDraw(pDev, Point(0,0), Size(aPageSize.Width, aPageSize.Height), JobSetup()); + vcl::PDFExtOutDevData* pPDFData = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + if (pPDFData && pPDFData->GetIsExportTaggedPDF() && bIsLastPage) + { + pPDFData->EndStructureElement(); // Workbook + assert(pPDFData->GetScPDFState()); + delete pPDFData->GetScPDFState(); + pPDFData->SetScPDFState(nullptr); + } + + lcl_PDFExportBookmarkHelper(pDev, rDoc, pPrintFuncCache, aMark, nVisTab); + return; } else if ( aMark.IsMarked() ) @@ -2304,131 +2479,31 @@ void SAL_CALL ScModelObj::render( sal_Int32 nSelRenderer, const uno::Any& aSelec tools::Long nDisplayStart = pPrintFuncCache->GetDisplayStart( nTab ); tools::Long nTabStart = pPrintFuncCache->GetTabStart( nTab ); - vcl::PDFExtOutDevData* pPDFData = dynamic_cast< vcl::PDFExtOutDevData* >(pDev->GetExtOutDevData() ); - if ( nRenderer == nTabStart ) + if ( nRenderer == nTabStart || bIsFirstPage ) { - if (pPDFData) - { - css::lang::Locale const docLocale(Application::GetSettings().GetLanguageTag().getLocale()); - pPDFData->SetDocumentLocale(docLocale); - } - - // first page of a sheet: add outline item for the sheet name - - if ( pPDFData && pPDFData->GetIsExportBookmarks() ) - { - // the sheet starts at the top of the page - tools::Rectangle aArea( pDev->PixelToLogic( tools::Rectangle( 0,0,0,0 ) ) ); - sal_Int32 nDestID = pPDFData->CreateDest( aArea ); - OUString aTabName; - rDoc.GetName( nTab, aTabName ); - // top-level - pPDFData->CreateOutlineItem( -1/*nParent*/, aTabName, nDestID ); - } - // #i56629# add the named destination stuff - if( pPDFData && pPDFData->GetIsExportNamedDestinations() ) - { - tools::Rectangle aArea( pDev->PixelToLogic( tools::Rectangle( 0,0,0,0 ) ) ); - OUString aTabName; - rDoc.GetName( nTab, aTabName ); - //need the PDF page number here - pPDFData->CreateNamedDest( aTabName, aArea ); - } + OUString aTabName; + rDoc.GetName(nTab, aTabName); + lcl_PDFExportHelper(pDev, aTabName, bIsFirstPage); } (void)pPrintFunc->DoPrint( aPage, nTabStart, nDisplayStart, true, nullptr ); + vcl::PDFExtOutDevData* pPDFData = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + if (pPDFData && pPDFData->GetIsExportTaggedPDF() && bIsLastPage) + { + pPDFData->EndStructureElement(); // Workbook + assert(pPDFData->GetScPDFState()); + delete pPDFData->GetScPDFState(); + pPDFData->SetScPDFState(nullptr); + } + if (!m_pPrintState) { m_pPrintState.reset(new ScPrintState()); pPrintFunc->GetPrintState(*m_pPrintState, true); } - // resolve the hyperlinks for PDF export - - if ( !pPDFData || pPDFData->GetBookmarks().empty() ) - return; - - // iterate over the hyperlinks that were output for this page - - std::vector< vcl::PDFExtOutDevBookmarkEntry >& rBookmarks = pPDFData->GetBookmarks(); - for ( const auto& rBookmark : rBookmarks ) - { - OUString aBookmark = rBookmark.aBookmark; - if ( aBookmark.toChar() == '#' ) - { - // try to resolve internal link - - OUString aTarget( aBookmark.copy( 1 ) ); - - ScRange aTargetRange; - tools::Rectangle aTargetRect; // 1/100th mm - bool bIsSheet = false; - bool bValid = lcl_ParseTarget( aTarget, aTargetRange, aTargetRect, bIsSheet, rDoc, nTab ); - - if ( bValid ) - { - sal_Int32 nPage = -1; - tools::Rectangle aArea; - if ( bIsSheet ) - { - // Get first page for sheet (if nothing from that sheet is printed, - // this page can show a different sheet) - nPage = pPrintFuncCache->GetTabStart( aTargetRange.aStart.Tab() ); - aArea = pDev->PixelToLogic( tools::Rectangle( 0,0,0,0 ) ); - } - else - { - pPrintFuncCache->InitLocations( aMark, pDev ); // does nothing if already initialized - - ScPrintPageLocation aLocation; - if ( pPrintFuncCache->FindLocation( aTargetRange.aStart, aLocation ) ) - { - nPage = aLocation.nPage; - - // get the rectangle of the page's cell range in 1/100th mm - ScRange aLocRange = aLocation.aCellRange; - tools::Rectangle aLocationMM = rDoc.GetMMRect( - aLocRange.aStart.Col(), aLocRange.aStart.Row(), - aLocRange.aEnd.Col(), aLocRange.aEnd.Row(), - aLocRange.aStart.Tab() ); - tools::Rectangle aLocationPixel = aLocation.aRectangle; - - // Scale and move the target rectangle from aLocationMM to aLocationPixel, - // to get the target rectangle in pixels. - assert(aLocationPixel.GetWidth() != 0 && aLocationPixel.GetHeight() != 0); - - Fraction aScaleX( aLocationPixel.GetWidth(), aLocationMM.GetWidth() ); - Fraction aScaleY( aLocationPixel.GetHeight(), aLocationMM.GetHeight() ); - - tools::Long nX1 = aLocationPixel.Left() + static_cast<tools::Long>( Fraction( aTargetRect.Left() - aLocationMM.Left(), 1 ) * aScaleX ); - tools::Long nX2 = aLocationPixel.Left() + static_cast<tools::Long>( Fraction( aTargetRect.Right() - aLocationMM.Left(), 1 ) * aScaleX ); - tools::Long nY1 = aLocationPixel.Top() + static_cast<tools::Long>( Fraction( aTargetRect.Top() - aLocationMM.Top(), 1 ) * aScaleY ); - tools::Long nY2 = aLocationPixel.Top() + static_cast<tools::Long>( Fraction( aTargetRect.Bottom() - aLocationMM.Top(), 1 ) * aScaleY ); - - if ( nX1 > aLocationPixel.Right() ) nX1 = aLocationPixel.Right(); - if ( nX2 > aLocationPixel.Right() ) nX2 = aLocationPixel.Right(); - if ( nY1 > aLocationPixel.Bottom() ) nY1 = aLocationPixel.Bottom(); - if ( nY2 > aLocationPixel.Bottom() ) nY2 = aLocationPixel.Bottom(); - - // The link target area is interpreted using the device's MapMode at - // the time of the CreateDest call, so PixelToLogic can be used here, - // regardless of the MapMode that is actually selected. - aArea = pDev->PixelToLogic( tools::Rectangle( nX1, nY1, nX2, nY2 ) ); - } - } - - if ( nPage >= 0 ) - pPDFData->SetLinkDest( rBookmark.nLinkId, pPDFData->CreateDest( aArea, nPage ) ); - } - } - else - { - // external link, use as-is - pPDFData->SetLinkURL( rBookmark.nLinkId, aBookmark ); - } - } - rBookmarks.clear(); + lcl_PDFExportBookmarkHelper(pDev, rDoc, pPrintFuncCache, aMark, nTab); } // XLinkTargetSupplier diff --git a/sc/source/ui/view/output.cxx b/sc/source/ui/view/output.cxx index be482d70d03e..23f3cecb539f 100644 --- a/sc/source/ui/view/output.cxx +++ b/sc/source/ui/view/output.cxx @@ -297,6 +297,56 @@ void ScOutputData::SetSyntaxMode( bool bNewMode ) } } +bool ScOutputData::ReopenPDFStructureElement(vcl::PDFWriter::StructElement aType, SCROW nRow, SCCOL nCol) +{ + bool bReopenTag = false; + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(mpDev->GetExtOutDevData()); + if (pPDF) + { + if (aType == vcl::PDFWriter::Part) // Worksheet + { + if (pPDF->GetScPDFState()->m_WorksheetId != -1) + { + sal_Int32 nId = pPDF->GetScPDFState()->m_WorksheetId; + pPDF->BeginStructureElement(nId); + bReopenTag = true; + } + } + else if (aType == vcl::PDFWriter::Table) + { + if (pPDF->GetScPDFState()->m_TableId != -1) + { + sal_Int32 nId = pPDF->GetScPDFState()->m_TableId; + pPDF->BeginStructureElement(nId); + bReopenTag = true; + } + } + else if (aType == vcl::PDFWriter::TableRow) + { + const auto aIter = pPDF->GetScPDFState()->m_TableRowMap.find(nRow); + if (aIter != pPDF->GetScPDFState()->m_TableRowMap.end() && nRow == aIter->first) + { + sal_Int32 nId = (*aIter).second; + pPDF->BeginStructureElement(nId); + bReopenTag = true; + } + } + else if (aType == vcl::PDFWriter::TableData) + { + const std::pair<SCROW, SCCOL> keyToFind = std::make_pair(nRow, nCol); + const auto aIter = pPDF->GetScPDFState()->m_TableDataMap.find(keyToFind); + if (aIter != pPDF->GetScPDFState()->m_TableDataMap.end() && keyToFind == aIter->first) + { + sal_Int32 nId = (*aIter).second; + pPDF->BeginStructureElement(nId); + bReopenTag = true; + } + } + } + + return bReopenTag; +} + void ScOutputData::DrawGrid(vcl::RenderContext& rRenderContext, bool bGrid, bool bPage, bool bMergeCover) { // bMergeCover : Draw lines in sheet bgcolor to cover lok client grid lines in merged cell areas. @@ -1025,6 +1075,9 @@ void drawCells(vcl::RenderContext& rRenderContext, std::optional<Color> const & void ScOutputData::DrawBackground(vcl::RenderContext& rRenderContext) { + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(mpDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + Size aOnePixel = rRenderContext.PixelToLogic(Size(1,1)); tools::Long nOneXLogic = aOnePixel.Width(); tools::Long nOneYLogic = aOnePixel.Height(); @@ -1074,6 +1127,9 @@ void ScOutputData::DrawBackground(vcl::RenderContext& rRenderContext) } else { + if (bTaggedPDF) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::NonStructElement); + // scan for rows with the same background: SCSIZE nSkip = 0; while ( nArrY+nSkip+2<nArrCount && @@ -1181,6 +1237,9 @@ void ScOutputData::DrawBackground(vcl::RenderContext& rRenderContext) drawCells(rRenderContext, std::optional<Color>(), nullptr, pOldColor, pOldBackground, aRect, nPosXLogic, nLayoutSign, nOneXLogic, nOneYLogic, nullptr, pOldDataBarInfo, nullptr, pOldIconSetInfo, mpDoc->GetIconSetBitmapMap()); nArrY += nSkip; + + if (bTaggedPDF) + pPDF->EndStructureElement(); } } nPosY += nRowHeight; @@ -1194,6 +1253,9 @@ void ScOutputData::DrawShadow() void ScOutputData::DrawExtraShadow(bool bLeft, bool bTop, bool bRight, bool bBottom) { + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(mpDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + mpDev->SetLineColor(); const StyleSettings& rStyleSettings = Application::GetSettings().GetStyleSettings(); @@ -1235,6 +1297,9 @@ void ScOutputData::DrawExtraShadow(bool bLeft, bool bTop, bool bRight, bool bBot pThisRowInfo->cellInfo(nCol).pHShadowOrigin; if ( pAttr && !bSkipX ) { + if (bTaggedPDF) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::NonStructElement); + ScShadowPart ePart = nPass ? pThisRowInfo->cellInfo(nCol).eVShadowPart : pThisRowInfo->cellInfo(nCol).eHShadowPart; @@ -1322,6 +1387,9 @@ void ScOutputData::DrawExtraShadow(bool bLeft, bool bTop, bool bRight, bool bBot //! merge rectangles? mpDev->SetFillColor( bCellContrast ? aAutoTextColor : pAttr->GetColor() ); mpDev->DrawRect( aRect ); + + if (bTaggedPDF) + pPDF->EndStructureElement(); } } } @@ -1390,6 +1458,11 @@ static tools::Long lclGetSnappedY( const OutputDevice& rDev, tools::Long nPosY, void ScOutputData::DrawFrame(vcl::RenderContext& rRenderContext) { + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(mpDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + if (bTaggedPDF) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::NonStructElement); + DrawModeFlags nOldDrawMode = rRenderContext.GetDrawMode(); Color aSingleColor; @@ -1510,6 +1583,9 @@ void ScOutputData::DrawFrame(vcl::RenderContext& rRenderContext) pProcessor.reset(); rRenderContext.SetDrawMode(nOldDrawMode); + + if (bTaggedPDF) + pPDF->EndStructureElement(); } void ScOutputData::DrawRotatedFrame(vcl::RenderContext& rRenderContext) diff --git a/sc/source/ui/view/output2.cxx b/sc/source/ui/view/output2.cxx index d5e87a66d3d0..f9e4749377d7 100644 --- a/sc/source/ui/view/output2.cxx +++ b/sc/source/ui/view/output2.cxx @@ -1482,6 +1482,20 @@ void ScOutputData::DrawStrings( bool bPixelToLogic ) void ScOutputData::LayoutStrings(bool bPixelToLogic) { + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(mpDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + if (bTaggedPDF) + { + bool bReopenTag = ReopenPDFStructureElement(vcl::PDFWriter::Table); + if (!bReopenTag) + { + sal_Int32 nId = pPDF->EnsureStructureElement(nullptr); + pPDF->InitStructureElement(nId, vcl::PDFWriter::Table, "Table"); + pPDF->BeginStructureElement(nId); + pPDF->GetScPDFState()->m_TableId = nId; + } + } + bool bOrigIsInLayoutStrings = mpDoc->IsInLayoutStrings(); mpDoc->SetLayoutStrings(true); OSL_ENSURE( mpDev == mpRefDevice || @@ -1502,8 +1516,6 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) // for the entire formula group, which could be large. mpDoc->InterpretCellsIfNeeded( ScRange( nX1, nY1, nTab, nX2, nY2, nTab )); - vcl::PDFExtOutDevData* pPDFData = dynamic_cast< vcl::PDFExtOutDevData* >(mpDev->GetExtOutDevData() ); - ScDrawStringsVars aVars( this, bPixelToLogic ); bool bProgress = false; @@ -1521,7 +1533,7 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) } SCCOL nLoopStartX = nX1; - if ( nX1 > 0 ) + if ( nX1 > 0 && !bTaggedPDF ) --nLoopStartX; // start before nX1 for rest of long text to the left // variables for GetOutputArea @@ -1546,11 +1558,31 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) SCROW nY = pThisRowInfo->nRowNo; if (pThisRowInfo->bChanged) { + if (bTaggedPDF) + { + bool bReopenTag = false; + if (nLoopStartX != 0) + { + bReopenTag + = ReopenPDFStructureElement(vcl::PDFWriter::TableRow, nY); + } + if (!bReopenTag) + { + sal_Int32 nId = pPDF->EnsureStructureElement(nullptr); + pPDF->InitStructureElement(nId, vcl::PDFWriter::TableRow, "TR"); + pPDF->BeginStructureElement(nId); + pPDF->GetScPDFState()->m_TableRowMap.emplace(nY, nId); + } + } + tools::Long nPosX = nInitPosX; if ( nLoopStartX < nX1 ) nPosX -= pRowInfo[0].basicCellInfo(nLoopStartX).nWidth * nLayoutSign; for (SCCOL nX=nLoopStartX; nX<=nX2; nX++) { + if (bTaggedPDF) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::TableData, "TD"); + bool bMergeEmpty = false; const ScCellInfo* pInfo = &pThisRowInfo->cellInfo(nX); bool bEmpty = nX < nX1 || pThisRowInfo->basicCellInfo(nX).bEmptyCellText; @@ -1880,6 +1912,13 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) RowInfo* pMarkRowInfo = ( nCellY == nY ) ? pThisRowInfo : &pRowInfo[0]; pMarkRowInfo->basicCellInfo(nMarkX).bEditEngine = true; bDoCell = false; // don't draw here + + // Mark the tagged "TD" structure element to be drawn in DrawEdit + if (bTaggedPDF) + { + sal_Int32 nId = pPDF->GetCurrentStructureElement(); + pPDF->GetScPDFState()->m_TableDataMap[{nY, nX}] = nId; + } } if ( bDoCell ) { @@ -2088,6 +2127,9 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) const OUString& aString = aVars.GetString(); if (!aString.isEmpty()) { + if (bTaggedPDF) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::Paragraph, "P"); + // If the string is clipped, make it shorter for // better performance since drawing by HarfBuzz is // quite expensive especially for long string. @@ -2154,6 +2196,8 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) mpDev->DrawText(aDrawTextPos, aShort, 0, -1, nullptr, nullptr, aVars.GetLayoutGlyphs(aShort)); } + if (bTaggedPDF) + pPDF->EndStructureElement(); } if ( bHClip || bVClip ) @@ -2165,7 +2209,7 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) } // PDF: whole-cell hyperlink from formula? - bool bHasURL = pPDFData && aCell.getType() == CELLTYPE_FORMULA && aCell.getFormula()->IsHyperLinkCell(); + bool bHasURL = pPDF && aCell.getType() == CELLTYPE_FORMULA && aCell.getFormula()->IsHyperLinkCell(); if (bHasURL) { tools::Rectangle aURLRect( aURLStart, aVars.GetTextSize() ); @@ -2174,10 +2218,17 @@ void ScOutputData::LayoutStrings(bool bPixelToLogic) } } nPosX += pRowInfo[0].basicCellInfo(nX).nWidth * nLayoutSign; + if (bTaggedPDF) + pPDF->EndStructureElement(); } + if (bTaggedPDF) + pPDF->EndStructureElement(); } nPosY += pRowInfo[nArrY].nHeight; } + if (bTaggedPDF) + pPDF->EndStructureElement(); + if ( bProgress ) ScProgress::DeleteInterpretProgress(); } @@ -4384,6 +4435,9 @@ void ScOutputData::DrawEditAsianVertical(DrawEditParam& rParam) void ScOutputData::DrawEdit(bool bPixelToLogic) { + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(mpDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + InitOutputEditEngine(); bool bHyphenatorSet = false; @@ -4425,6 +4479,10 @@ void ScOutputData::DrawEdit(bool bPixelToLogic) { SCROW nY = pThisRowInfo->nRowNo; + bool bReopenTag = false; + if (bTaggedPDF) + bReopenTag = ReopenPDFStructureElement(vcl::PDFWriter::TableData, nY, nX); + SCCOL nCellX = nX; // position where the cell really starts SCROW nCellY = nY; bool bDoCell = false; @@ -4557,6 +4615,8 @@ void ScOutputData::DrawEdit(bool bPixelToLogic) pOldPreviewFontSet = aParam.mpOldPreviewFontSet; bHyphenatorSet = aParam.mbHyphenatorSet; } + if (bReopenTag) + pPDF->EndStructureElement(); } nPosX += pRowInfo[0].basicCellInfo(nX).nWidth * nLayoutSign; } diff --git a/sc/source/ui/view/printfun.cxx b/sc/source/ui/view/printfun.cxx index db81b8692849..f5f92b7f6876 100644 --- a/sc/source/ui/view/printfun.cxx +++ b/sc/source/ui/view/printfun.cxx @@ -29,6 +29,7 @@ #include <svtools/colorcfg.hxx> #include <editeng/editstat.hxx> #include <svx/fmview.hxx> +#include <vcl/pdfextoutdevdata.hxx> #include <editeng/frmdiritem.hxx> #include <editeng/lrspitem.hxx> #include <editeng/paperinf.hxx> @@ -576,6 +577,20 @@ void ScPrintFunc::DrawToDev(ScDocument& rDoc, OutputDevice* pDev, double /* nPri aOutputData.SetShowNullValues(bNullVal); aOutputData.SetShowFormulas(bFormula); + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + if (bTaggedPDF) + { + bool bReopen = aOutputData.ReopenPDFStructureElement(vcl::PDFWriter::Part); + if (!bReopen) + { + sal_Int32 nId = pPDF->EnsureStructureElement(nullptr); + pPDF->InitStructureElement(nId, vcl::PDFWriter::Part, "Worksheet"); + pPDF->BeginStructureElement(nId); + pPDF->GetScPDFState()->m_WorksheetId = nId; + } + } + ScDrawLayer* pModel = rDoc.GetDrawLayer(); std::unique_ptr<FmFormView> pDrawView; @@ -651,6 +666,10 @@ void ScPrintFunc::DrawToDev(ScDocument& rDoc, OutputDevice* pDev, double /* nPri // #i72502# aOutputData.PrintDrawingLayer(SC_LAYER_FRONT, aMMOffset); + + if (bTaggedPDF) + pPDF->EndStructureElement(); + aOutputData.PrintDrawingLayer(SC_LAYER_INTERN, aMMOffset); aOutputData.PostPrintDrawingLayer(aMMOffset); // #i74768# } @@ -1622,6 +1641,20 @@ void ScPrintFunc::PrintArea( SCCOL nX1, SCROW nY1, SCCOL nX2, SCROW nY2, ScOutputData aOutputData( pDev, OUTTYPE_PRINTER, aTabInfo, &rDoc, nPrintTab, nScrX, nScrY, nX1, nY1, nX2, nY2, nScaleX, nScaleY ); + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + if (bTaggedPDF) + { + bool bReopen = aOutputData.ReopenPDFStructureElement(vcl::PDFWriter::Part); + if (!bReopen) + { + sal_Int32 nId = pPDF->EnsureStructureElement(nullptr); + pPDF->InitStructureElement(nId, vcl::PDFWriter::Part, "Worksheet"); + pPDF->BeginStructureElement(nId); + pPDF->GetScPDFState()->m_WorksheetId = nId; + } + } + aOutputData.SetDrawView( pDrawView ); // test if all paint parts are hidden, then a paint is not necessary at all @@ -1690,9 +1723,17 @@ void ScPrintFunc::PrintArea( SCCOL nX1, SCROW nY1, SCCOL nX2, SCROW nY2, aOutputData.PrintDrawingLayer(SC_LAYER_FRONT, aMMOffset); } + if (bTaggedPDF) + { + aOutputData.PrintDrawingLayer(SC_LAYER_CONTROLS, aMMOffset); + pPDF->EndStructureElement(); + } + // #i72502# aOutputData.PrintDrawingLayer(SC_LAYER_INTERN, aMMOffset); - aOutputData.PostPrintDrawingLayer(aMMOffset); // #i74768# + + if (!bTaggedPDF) + aOutputData.PostPrintDrawingLayer(aMMOffset); // #i74768# } bool ScPrintFunc::IsMirror( tools::Long nPageNo ) // Mirror margins? @@ -1763,6 +1804,11 @@ void ScPrintFunc::MakeEditEngine() void ScPrintFunc::PrintHF( tools::Long nPageNo, bool bHeader, tools::Long nStartY, bool bDoPrint, ScPreviewLocationData* pLocationData ) { + vcl::PDFExtOutDevData* pPDF = dynamic_cast<vcl::PDFExtOutDevData*>(pDev->GetExtOutDevData()); + bool bTaggedPDF = pPDF && pPDF->GetIsExportTaggedPDF(); + if (bTaggedPDF) + pPDF->WrapBeginStructureElement(vcl::PDFWriter::NonStructElement); + const ScPrintHFParam& rParam = bHeader ? aHdr : aFtr; pDev->SetMapMode( aTwipMode ); // Head-/Footlines in Twips @@ -1896,6 +1942,9 @@ void ScPrintFunc::PrintHF( tools::Long nPageNo, bool bHeader, tools::Long nStart tools::Rectangle aHeaderRect( aBorderStart, aBorderSize ); pLocationData->AddHeaderFooter( aHeaderRect, bHeader, bLeft ); } + + if (bTaggedPDF) + pPDF->EndStructureElement(); } tools::Long ScPrintFunc::DoNotes( tools::Long nNoteStart, bool bDoPrint, ScPreviewLocationData* pLocationData )