slideshow: simplify rendering, render text fields each in own layer

This change significantly simplifies the rendering of layers by
introducing an analysis pass (when we render the background) that
creates rendering pass objects, which contain which objects and
paragraphs need to be rendered in each pass. Previously we tried
to figure this out durign the rendering, which meant we had to
track a lot of various states. This all is not longer needed. This
chaneg also means that the new RenderPassObjectRedirector is fairly
simple.

In addition this changes the test and implementation to render
each text field in its own layer as it is expected by the online
implementation (to enable master pages caching), but it still
doesn't first render the placeholders for text fields as MasterPage
layer and the TextField layer as a separate bitmap layer. This
will be done next.

Change-Id: I5377f4c5e5ebff67da70b248c831837cb9f2e559
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/182923
Tested-by: Jenkins
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
This commit is contained in:
Tomaž Vajngerl
2025-03-14 17:17:03 +01:00
committed by Miklos Vajna
parent 05f2cb64f6
commit 7aff1587cd
4 changed files with 222 additions and 158 deletions
+27 -1
View File
@@ -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<sal_uInt8> 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);
+32 -41
View File
@@ -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<sal_Int32, AnimationLayerInfo> maParagraphInfos;
};
// Holds information used when doing one rendering pass
struct RenderPass
{
RenderStage meStage = RenderStage::Background;
std::unordered_map<SdrObject*, std::deque<sal_Int32>> 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<RenderPass> maRenderPasses;
RenderStage meStage = RenderStage::Background;
bool mbStopRenderingWhenField = true;
std::unordered_set<SdrObject*> maObjectsDone;
std::unordered_map<SdrObject*, AnimationRenderInfo> maAnimationRenderInfoList;
sal_Int32 mnIndex[static_cast<unsigned>(RenderStage::Count)] = { 0, 0, 0, 0 };
std::array<sal_Int32, 4> 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<sal_Int32> maParagraphsToRender;
/// increments index depending on the current render stage
void incrementIndex() { mnIndex[static_cast<unsigned>(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<unsigned>(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();
+163 -116
View File
@@ -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<VirtualDevice> 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<sal_Int32> 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<sal_Int32> 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<SdrTextObj*>(pObject);
if (pTextObject)
{
sal_Int32 nNumberOfParagraphs = pTextObject->GetOutlinerParaObject()->Count();
std::deque<sal_Int32> 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<sal_Int32>{ 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<sal_Int32>());
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<sal_Int32>());
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<sal_Int32>());
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<drawinglayer::primitive2d::Primitive2DContainer&>(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<sal_Int32> 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<drawing::XShape> 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;
}