diff --git a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp index 960adc4c5c55..1d7cc1f15e05 100644 Binary files a/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp and b/sd/qa/unit/tiledrendering/data/SlideRenderingTest_WithFields.odp differ diff --git a/sd/qa/unit/tiledrendering/tiledrendering.cxx b/sd/qa/unit/tiledrendering/tiledrendering.cxx index 738cd9d2cd73..2790ce524015 100644 --- a/sd/qa/unit/tiledrendering/tiledrendering.cxx +++ b/sd/qa/unit/tiledrendering/tiledrendering.cxx @@ -2929,6 +2929,7 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFie double dScale = 1.0; OUString rJsonMsg; CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, dScale, rJsonMsg)); + debugWriteImageToFile(0, pBuffer, nViewWidth, nViewHeight, rJsonMsg.toUtf8().getStr()); } { @@ -2968,6 +2969,31 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFie // top-left corner CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(20, 20)); + // bottom-left corner + CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(20, nViewHeight - 20)); + + // bottom-right corner + CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(nViewWidth - 20, nViewHeight - 20)); + + // bottom-center + CPPUNIT_ASSERT_EQUAL(Color(0x20, 0xaa, 0x00), aBitmapEx.GetPixelColor(nViewWidth / 2, nViewHeight - 20)); + } + + { + std::vector pBuffer(nViewWidth * nViewHeight * 4); + bool bIsBitmapLayer = false; + double dScale = 1.0; + OUString rJsonMsg; + CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, dScale, rJsonMsg)); + CPPUNIT_ASSERT(bIsBitmapLayer); + // TODO - check JSON content + debugWriteImageToFile(3, pBuffer, nViewWidth, nViewHeight, rJsonMsg.toUtf8().getStr()); + + BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true); + + // top-left corner + CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(20, 20)); + // bottom-left corner CPPUNIT_ASSERT_EQUAL(Color(0x90, 0x80, 0xff), aBitmapEx.GetPixelColor(20, nViewHeight - 20)); @@ -2983,7 +3009,7 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFie CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, dScale, rJsonMsg)); CPPUNIT_ASSERT(bIsBitmapLayer); // TODO - check JSON content - debugWriteImageToFile(3, pBuffer, nViewWidth, nViewHeight, rJsonMsg.toUtf8().getStr()); + debugWriteImageToFile(4, pBuffer, nViewWidth, nViewHeight, rJsonMsg.toUtf8().getStr()); BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true); diff --git a/sd/source/ui/inc/SlideshowLayerRenderer.hxx b/sd/source/ui/inc/SlideshowLayerRenderer.hxx index 858ab08178b8..cfadb19ba346 100644 --- a/sd/source/ui/inc/SlideshowLayerRenderer.hxx +++ b/sd/source/ui/inc/SlideshowLayerRenderer.hxx @@ -34,17 +34,21 @@ namespace com::sun::star::animations class XAnimate; } +namespace sdr::contact +{ +class ViewObjectContactRedirector; +} + namespace sd { -struct RenderContext; +class RenderContext; enum class RenderStage { - Background, - Master, - Slide, - TextFields, - Count + Background = 0, + Master = 1, + Slide = 2, + TextFields = 3, }; struct AnimationLayerInfo @@ -60,31 +64,36 @@ struct AnimationRenderInfo std::unordered_map maParagraphInfos; }; +// Holds information used when doing one rendering pass +struct RenderPass +{ + RenderStage meStage = RenderStage::Background; + std::unordered_map> maObjectsAndParagraphs; + bool mbRenderObjectBackground = false; + + bool mbAnimation = false; + SdrObject* mpAnimatedObject = nullptr; + sal_Int32 mnAnimatedParagraph = -1; + + bool isEmpty() { return maObjectsAndParagraphs.empty(); } +}; + /** Holds rendering state, properties and switches through all rendering passes */ struct RenderState { + std::deque maRenderPasses; + RenderStage meStage = RenderStage::Background; - bool mbStopRenderingWhenField = true; - - std::unordered_set maObjectsDone; - std::unordered_map maAnimationRenderInfoList; - sal_Int32 mnIndex[static_cast(RenderStage::Count)] = { 0, 0, 0, 0 }; + std::array maIndices = { 0, 0, 0, 0 }; + SdrObject* mpCurrentTarget = nullptr; sal_Int32 mnCurrentTargetParagraph = -1; - sal_Int32 mnRenderedObjectsInPass = 0; - - bool mbSkipAllInThisPass = false; - - sal_Int32 mnCurrentPass = 0; - - std::deque maParagraphsToRender; - /// increments index depending on the current render stage - void incrementIndex() { mnIndex[static_cast(meStage)]++; } + void incrementIndex() { maIndices[size_t(meStage)]++; } /// returns the current stage as string OString stageString() const @@ -99,7 +108,7 @@ struct RenderState } /// returns the current index depending on the current render stage - sal_Int32 currentIndex() const { return mnIndex[static_cast(meStage)]; } + sal_Int32 currentIndex() const { return maIndices[size_t(meStage)]; } /// returns the current target element for which layer is created if any SdrObject* currentTarget() const { return mpCurrentTarget; } @@ -110,31 +119,12 @@ struct RenderState /// resets properties that are valid for one pass void resetPass() { - mnRenderedObjectsInPass = 0; - mbSkipAllInThisPass = false; mpCurrentTarget = nullptr; mnCurrentTargetParagraph = -1; } - bool hasPassAnyRenderedOutput() const { return mnRenderedObjectsInPass > 0; } - - /// is first rendered object in pass - bool isFirstObjectInPass() const { return mnRenderedObjectsInPass == 0; } - - /// return if there was no rendering output in the pass - bool noMoreOutput() const - { - // no output and we didn't skip anything and nothing was rendered - return !hasPassAnyRenderedOutput() && !mbSkipAllInThisPass; - } - /// should include background in rendering bool includeBackground() const { return meStage == RenderStage::Background; } - - bool isObjectAlreadyRendered(SdrObject* pObject) const - { - return maObjectsDone.find(pObject) != maObjectsDone.end(); - } }; /** Renders a slide */ @@ -146,7 +136,8 @@ private: Size maSlideSize; RenderState maRenderState; - void createViewAndDraw(RenderContext& rRenderContext); + void createViewAndDraw(RenderContext& rRenderContext, + sdr::contact::ViewObjectContactRedirector* pRedirector); void writeJSON(OString& rJsonMsg); void setupAnimations(); diff --git a/sd/source/ui/tools/SlideshowLayerRenderer.cxx b/sd/source/ui/tools/SlideshowLayerRenderer.cxx index 46469f88b856..c85adf5ec8ab 100644 --- a/sd/source/ui/tools/SlideshowLayerRenderer.cxx +++ b/sd/source/ui/tools/SlideshowLayerRenderer.cxx @@ -44,11 +44,15 @@ using namespace ::com::sun::star; namespace sd { -struct RenderContext +/// Sets up the virtual device for rendering, and cleans up afterwards +class RenderContext { +private: SdrModel& mrModel; EEControlBits mnSavedControlBits; + +public: ScopedVclPtrInstance maVirtualDevice; RenderContext(unsigned char* pBuffer, SdrModel& rModel, SdrPage& rPage, Size const& rSlideSize, @@ -105,6 +109,7 @@ bool hasFields(SdrObject* pObject) return false; } +/// Sets visible for all kinds of polypolys in the container void changePolyPolys(drawinglayer::primitive2d::Primitive2DContainer& rContainer, bool bRenderObject) { @@ -121,6 +126,7 @@ void changePolyPolys(drawinglayer::primitive2d::Primitive2DContainer& rContainer } } +/// Searches for rectangle primitive and changes if the background should be rendered void changeBackground(drawinglayer::primitive2d::Primitive2DContainer const& rContainer, bool bRenderObject) { @@ -137,6 +143,7 @@ void changeBackground(drawinglayer::primitive2d::Primitive2DContainer const& rCo } } +/// Find the text block in the primitive containder, decompose if necessary drawinglayer::primitive2d::TextHierarchyBlockPrimitive2D* findTextBlock(drawinglayer::primitive2d::Primitive2DContainer const& rContainer, drawinglayer::geometry::ViewInformation2D const& rViewInformation2D) @@ -181,6 +188,7 @@ findTextBlock(drawinglayer::primitive2d::Primitive2DContainer const& rContainer, return nullptr; } +/// show/hide paragraphs in the container void modifyParagraphs(drawinglayer::primitive2d::Primitive2DContainer& rContainer, drawinglayer::geometry::ViewInformation2D const& rViewInformation2D, std::deque const& rPreserveIndices, bool bRenderObject) @@ -216,44 +224,51 @@ void modifyParagraphs(drawinglayer::primitive2d::Primitive2DContainer& rContaine } } -/** VOC redirector to control which object should be rendered and which not */ -class ObjectRedirector : public sdr::contact::ViewObjectContactRedirector +/// Analyze the renderng and create rendering passes +class AnalyzeRenderingRedirector : public sdr::contact::ViewObjectContactRedirector { protected: RenderState& mrRenderState; + RenderPass* mpCurrentRenderPass; + RenderStage mePreviousStage = RenderStage::Master; + public: - ObjectRedirector(RenderState& rRenderState) + AnalyzeRenderingRedirector(RenderState& rRenderState) : mrRenderState(rRenderState) + , mpCurrentRenderPass(newRenderPass()) { } + // Adds a new rendering pass to the list and returns it + RenderPass* newRenderPass() + { + mrRenderState.maRenderPasses.emplace_back(); + return &mrRenderState.maRenderPasses.back(); + } + + // Closes current rendering pass, and creates a new empty current one + void closeRenderPass() + { + if (mpCurrentRenderPass->maObjectsAndParagraphs.empty()) + return; + + mpCurrentRenderPass = newRenderPass(); + } + virtual void createRedirectedPrimitive2DSequence( const sdr::contact::ViewObjectContact& rOriginal, const sdr::contact::DisplayInfo& rDisplayInfo, - drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) override + drawinglayer::primitive2d::Primitive2DDecompositionVisitor& /*rVisitor*/) override { - // Generate single pass for background layer - if (mrRenderState.meStage == RenderStage::Background) - { - mrRenderState.mnRenderedObjectsInPass++; - mrRenderState.mbSkipAllInThisPass = true; - return; - } - - if (mrRenderState.mbSkipAllInThisPass) - return; - SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject(); - drawinglayer::geometry::ViewInformation2D const& rViewInformation2D - = rOriginal.GetObjectContact().getViewInformation2D(); - // Check if we are rendering an object that is valid to render (exists, and not empty) if (pObject == nullptr || pObject->IsEmptyPresObj()) return; SdrPage* pPage = pObject->getSdrPageFromSdrObject(); + // Does the object have a page if (pPage == nullptr) return; @@ -265,124 +280,134 @@ public: if (!bVisible) return; - // Check if we have already rendered the object - if (mrRenderState.isObjectAlreadyRendered(pObject)) - return; + // Determine the current stage, depending on the page + RenderStage eCurrentStage + = pPage->IsMasterPage() ? RenderStage::Master : RenderStage::Slide; - // Check if we are in correct stage - if (mrRenderState.meStage == RenderStage::Master && !pPage->IsMasterPage()) - { - if (mrRenderState.isFirstObjectInPass()) - { - // if this is the first object - change from master to slide - // means we are done with rendering of master layers - mrRenderState.meStage = RenderStage::Slide; - } - else - { - // if not, we have to stop rendering all further objects - mrRenderState.mbSkipAllInThisPass = true; - return; - } - } + // We switched from master objecst to slide objects + if (eCurrentStage == RenderStage::Slide && mePreviousStage == RenderStage::Master) + closeRenderPass(); - // Paragraph rendering switches - bool bRenderOtherParagraphs = false; - std::deque nOtherParagraphs; - - // check if object is in animation + // check if object is in an animation auto aIterator = mrRenderState.maAnimationRenderInfoList.find(pObject); if (aIterator != mrRenderState.maAnimationRenderInfoList.end()) { - // Animated object has to be only one in the render - mrRenderState.mbSkipAllInThisPass = true; // skip all next objects - - // Force a new layer - if (!mrRenderState.isFirstObjectInPass()) - return; + closeRenderPass(); AnimationRenderInfo aInfo = aIterator->second; - if (mrRenderState.maParagraphsToRender.empty() - && !aInfo.maParagraphs.empty()) // we need to render paragraphs + if (!aInfo.maParagraphs.empty()) // we need to render paragraphs { auto* pTextObject = dynamic_cast(pObject); if (pTextObject) { sal_Int32 nNumberOfParagraphs = pTextObject->GetOutlinerParaObject()->Count(); + std::deque nOtherParagraphs; for (sal_Int32 nParagraph = 0; nParagraph < nNumberOfParagraphs; ++nParagraph) - nOtherParagraphs.push_back(nParagraph); + { + if (std::find(aInfo.maParagraphs.begin(), aInfo.maParagraphs.end(), + nParagraph) + == aInfo.maParagraphs.end()) + nOtherParagraphs.push_back(nParagraph); + } + // Add the non-animated part of the object that has animated paragraphs + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, nOtherParagraphs); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbRenderObjectBackground = true; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpAnimatedObject = pObject; + closeRenderPass(); + // Add all the animated paragraphs for (sal_Int32 nParagraph : aInfo.maParagraphs) { - mrRenderState.maParagraphsToRender.push_back(nParagraph); - std::erase(nOtherParagraphs, nParagraph); + mpCurrentRenderPass->maObjectsAndParagraphs.emplace( + pObject, std::deque{ nParagraph }); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpAnimatedObject = pObject; + mpCurrentRenderPass->mnAnimatedParagraph = nParagraph; + closeRenderPass(); } - bRenderOtherParagraphs = true; } } + else + { + // Add the animated object - paragraphs are not animated + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, + std::deque()); + mpCurrentRenderPass->meStage = eCurrentStage; + mpCurrentRenderPass->mbAnimation = true; + mpCurrentRenderPass->mpAnimatedObject = pObject; + closeRenderPass(); + } } - else if (mrRenderState.meStage == RenderStage::Master && hasFields(pObject) - && mrRenderState.mbStopRenderingWhenField && !mrRenderState.isFirstObjectInPass()) + // Check if the object has fields (slide number) + else if (eCurrentStage == RenderStage::Master && hasFields(pObject)) { - mrRenderState.mbStopRenderingWhenField = false; - mrRenderState.mbSkipAllInThisPass = true; - return; - } + closeRenderPass(); - mrRenderState.mpCurrentTarget = pObject; + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, std::deque()); + mpCurrentRenderPass->meStage = eCurrentStage; + closeRenderPass(); + } + // No specal handling is needed, just add the object to the current rendering pass + else + { + mpCurrentRenderPass->maObjectsAndParagraphs.emplace(pObject, std::deque()); + mpCurrentRenderPass->meStage = eCurrentStage; + } + mePreviousStage = eCurrentStage; + } +}; + +/// Render one render pass +class RenderPassObjectRedirector : public sdr::contact::ViewObjectContactRedirector +{ +protected: + RenderState& mrRenderState; + RenderPass const& mrRenderPass; + +public: + RenderPassObjectRedirector(RenderState& rRenderState, RenderPass const& rRenderPass) + : mrRenderState(rRenderState) + , mrRenderPass(rRenderPass) + { + } + + virtual void createRedirectedPrimitive2DSequence( + const sdr::contact::ViewObjectContact& rOriginal, + const sdr::contact::DisplayInfo& rDisplayInfo, + drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) override + { + SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject(); + + if (!pObject) + return; + + // check if object is in animation + auto aIterator = mrRenderPass.maObjectsAndParagraphs.find(pObject); + if (aIterator == mrRenderPass.maObjectsAndParagraphs.end()) + return; // render the object sdr::contact::ViewObjectContactRedirector::createRedirectedPrimitive2DSequence( rOriginal, rDisplayInfo, rVisitor); - if (!mrRenderState.maParagraphsToRender.empty()) + auto const& rParagraphs = aIterator->second; + + if (!rParagraphs.empty()) { + auto const& rViewInformation2D = rOriginal.GetObjectContact().getViewInformation2D(); auto rContainer = static_cast(rVisitor); - - if (bRenderOtherParagraphs) - { - modifyParagraphs(rContainer, rViewInformation2D, nOtherParagraphs, - true); // render the object - mrRenderState.mnCurrentTargetParagraph = -1; - } - else - { - sal_Int32 nParagraph = mrRenderState.maParagraphsToRender.front(); - mrRenderState.maParagraphsToRender.pop_front(); - - std::deque aPreserveParagraphs{ nParagraph }; - mrRenderState.mnCurrentTargetParagraph = nParagraph; - // render only the paragraphs - modifyParagraphs(rContainer, rViewInformation2D, aPreserveParagraphs, false); - } - } - - if (mrRenderState.maParagraphsToRender.empty()) - { - mrRenderState.mnRenderedObjectsInPass++; - mrRenderState.maObjectsDone.insert(pObject); + modifyParagraphs(rContainer, rViewInformation2D, rParagraphs, + mrRenderPass.mbRenderObjectBackground); } } }; -bool hasEmptyMaster(SdrPage const& rPage) -{ - if (!rPage.TRG_HasMasterPage()) - return true; - - SdrPage& rMaster = rPage.TRG_GetMasterPage(); - for (size_t i = 0; i < rMaster.GetObjCount(); i++) - { - auto pObject = rMaster.GetObj(i); - if (!pObject->IsEmptyPresObj()) - return false; - } - return true; -} - SdrObject* getObjectForShape(uno::Reference const& xShape) { if (!xShape.is()) @@ -506,7 +531,8 @@ Size SlideshowLayerRenderer::calculateAndSetSizePixel(Size const& rDesiredSizePi return maSlideSize; } -void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext) +void SlideshowLayerRenderer::createViewAndDraw( + RenderContext& rRenderContext, sdr::contact::ViewObjectContactRedirector* pRedirector) { SdrView aView(mrModel, rRenderContext.maVirtualDevice); aView.SetPageVisible(false); @@ -523,8 +549,7 @@ void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext) Point aPoint; vcl::Region aRegion(::tools::Rectangle(aPoint, aPageSize)); - ObjectRedirector aRedirector(maRenderState); - aView.CompleteRedraw(rRenderContext.maVirtualDevice, aRegion, &aRedirector); + aView.CompleteRedraw(rRenderContext.maVirtualDevice, aRegion, pRedirector); } namespace @@ -622,21 +647,43 @@ bool SlideshowLayerRenderer::render(unsigned char* pBuffer, double& rScale, OStr maRenderState.resetPass(); RenderContext aRenderContext(pBuffer, mrModel, mrPage, maSlideSize, Fraction(rScale)); - createViewAndDraw(aRenderContext); - - // Check if we are done rendering all passes and there is no more output - if (maRenderState.noMoreOutput()) - return false; - - writeJSON(rJsonMsg); - - maRenderState.mnCurrentPass++; + // Render Background and analyze other passes if (maRenderState.meStage == RenderStage::Background) - maRenderState.meStage = RenderStage::Master; + { + // Render no objects, just the background, but analyze and create rendering passes + AnalyzeRenderingRedirector aRedirector(maRenderState); + createViewAndDraw(aRenderContext, &aRedirector); - if (hasEmptyMaster(mrPage)) - maRenderState.meStage = RenderStage::Slide; + // Last rendering pass might be empty - delete + if (maRenderState.maRenderPasses.back().isEmpty()) + maRenderState.maRenderPasses.pop_back(); + + // Write JSON for the Background layer + writeJSON(rJsonMsg); + + maRenderState.meStage = RenderStage::Master; + } + else + { + if (maRenderState.maRenderPasses.empty()) + return false; + + auto const& rRenderPass = maRenderState.maRenderPasses.front(); + maRenderState.meStage = rRenderPass.meStage; + RenderPassObjectRedirector aRedirector(maRenderState, rRenderPass); + createViewAndDraw(aRenderContext, &aRedirector); + + if (rRenderPass.mbAnimation) + { + maRenderState.mpCurrentTarget = rRenderPass.mpAnimatedObject; + maRenderState.mnCurrentTargetParagraph = rRenderPass.mnAnimatedParagraph; + } + + writeJSON(rJsonMsg); + + maRenderState.maRenderPasses.pop_front(); + } return true; }