Make rendering more flexible, render fields in separate layer

If the amster page has a field (like slide number) then render the
objects containing fields into a separate master slide layer.

Also change the rendering in such a way to be more flexible and
has less conditions. So in the renderer it doesn't really matter
if we are rendering a master slide or slide, what matters only is
if the object is already rendered and if we need to stop rendering
for some specified reason (like we encountered a field and need to
stop rendering and need to switch to a new layer).

Change-Id: I37ea2528427bbc1b3de938f960fb344866ee9399
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/176911
Tested-by: Jenkins
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
This commit is contained in:
Tomaž Vajngerl 2024-08-21 11:25:05 +02:00 committed by Miklos Vajna
parent 512ef23224
commit a44a406de4
4 changed files with 295 additions and 95 deletions

View File

@ -3119,12 +3119,8 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testPresentationInfo)
CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering) CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering)
{ {
// Check rendering of slideshow layers (as in the document): // Check rendering of slideshow layers (as in the document):
// - background layer
// - master slide layer // - master slide layer
// - main slide layer // - main slide layer
// The document has nothing set for the background, so it should be application color = white
// On the master slide there is a (blue) rectangle on the right side - top-left should be transparent
// On the main slide there is a (green) rectangle on the top-left size - right side should be transparent
const bool bOutputPNG = false; // Control layer output to PNG files const bool bOutputPNG = false; // Control layer output to PNG files
@ -3204,6 +3200,118 @@ CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering)
pXImpressDocument->postSlideshowCleanup(); pXImpressDocument->postSlideshowCleanup();
} }
CPPUNIT_TEST_FIXTURE(SdTiledRenderingTest, testSlideshowLayeredRendering_WithFields)
{
// Check rendering of slideshow layers (as in the document):
// - master slide layer
// - main slide layer
const bool bOutputPNG = false; // Control layer output to PNG files
SdXImpressDocument* pXImpressDocument = createDoc("SlideRenderingTest_WithFields.odp");
pXImpressDocument->initializeForTiledRendering(uno::Sequence<beans::PropertyValue>());
sd::ViewShell* pViewShell = pXImpressDocument->GetDocShell()->GetViewShell();
CPPUNIT_ASSERT(pViewShell);
SdPage* pPage = pViewShell->GetActualPage();
CPPUNIT_ASSERT(pPage);
sal_Int32 nViewWidth = 2000;
sal_Int32 nViewHeight = 2000;
CPPUNIT_ASSERT(pXImpressDocument->createSlideRenderer(0, nViewWidth, nViewHeight, true, true));
CPPUNIT_ASSERT_EQUAL(2000, nViewWidth);
CPPUNIT_ASSERT_EQUAL(1125, nViewHeight);
const Color aTransparentColor(ColorAlpha, 0x00000000);
{
std::vector<sal_uInt8> pBuffer(nViewWidth * nViewHeight * 4);
bool bIsBitmapLayer = false;
OUString rJsonMsg;
CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, rJsonMsg));
CPPUNIT_ASSERT(bIsBitmapLayer);
// TODO - check JSON content
// printf ("1 %s\n\n", rJsonMsg.toUtf8().getStr());
BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true);
if (bOutputPNG)
{
SvFileStream aStream("/home/quikee/XXX_01.png", StreamMode::WRITE | StreamMode::TRUNC);
vcl::PngImageWriter aPNGWriter(aStream);
aPNGWriter.write(aBitmapEx);
}
// 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(Color(0xff, 0xd0, 0x40), aBitmapEx.GetPixelColor(nViewWidth - 20, nViewHeight - 20));
}
{
std::vector<sal_uInt8> pBuffer(nViewWidth * nViewHeight * 4);
bool bIsBitmapLayer = false;
OUString rJsonMsg;
CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, rJsonMsg));
CPPUNIT_ASSERT(bIsBitmapLayer);
// TODO - check JSON content
// printf ("2 %s\n\n", rJsonMsg.toUtf8().getStr());
BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true);
if (bOutputPNG)
{
SvFileStream aStream("/home/quikee/XXX_02.png", StreamMode::WRITE | StreamMode::TRUNC);
vcl::PngImageWriter aPNGWriter(aStream);
aPNGWriter.write(aBitmapEx);
}
// 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));
// bottom-right corner
CPPUNIT_ASSERT_EQUAL(aTransparentColor, aBitmapEx.GetPixelColor(nViewWidth - 20, nViewHeight - 20));
}
{
std::vector<sal_uInt8> pBuffer(nViewWidth * nViewHeight * 4);
bool bIsBitmapLayer = false;
OUString rJsonMsg;
CPPUNIT_ASSERT(!pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, rJsonMsg));
CPPUNIT_ASSERT(bIsBitmapLayer);
// TODO - check JSON content
// printf ("3 %s\n\n", rJsonMsg.toUtf8().getStr());
BitmapEx aBitmapEx = vcl::bitmap::CreateFromData(pBuffer.data(), nViewWidth, nViewHeight, nViewWidth * 4, /*nBitsPerPixel*/32, true, true);
if (bOutputPNG)
{
SvFileStream aStream("/home/quikee/XXX_03.png", StreamMode::WRITE | StreamMode::TRUNC);
vcl::PngImageWriter aPNGWriter(aStream);
aPNGWriter.write(aBitmapEx);
}
// top-left corner
CPPUNIT_ASSERT_EQUAL(Color(0x00, 0x50, 0x90), 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));
}
{
std::vector<sal_uInt8> pBuffer(nViewWidth * nViewHeight * 4);
bool bIsBitmapLayer = false;
OUString rJsonMsg;
CPPUNIT_ASSERT(pXImpressDocument->renderNextSlideLayer(pBuffer.data(), bIsBitmapLayer, rJsonMsg));
}
pXImpressDocument->postSlideshowCleanup();
}
CPPUNIT_PLUGIN_IMPLEMENT(); CPPUNIT_PLUGIN_IMPLEMENT();
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ /* vim:set shiftwidth=4 softtabstop=4 expandtab: */

View File

@ -13,41 +13,125 @@
#include <tools/gen.hxx> #include <tools/gen.hxx>
#include <rtl/string.hxx> #include <rtl/string.hxx>
#include <deque> #include <deque>
#include <unordered_set>
class SdrPage; class SdrPage;
class SdrModel; class SdrModel;
class SdrObject;
class Size; class Size;
namespace sd namespace sd
{ {
struct RenderContext; struct RenderContext;
struct RenderOptions;
enum class SlideRenderStage enum class RenderStage
{ {
Master, Master,
Slide Slide
}; };
/** Holds rendering state, properties and switches through all rendering passes */
struct RenderState
{
RenderStage meStage = RenderStage::Master;
sal_Int32 mnMasterIndex = 0;
bool mbStopRenderingWhenField = true;
std::unordered_set<SdrObject*> maObjectsDone;
sal_Int32 mnIndex = 0;
bool mbFirstObjectInPass = true;
bool mbPassHasOutput = false;
bool mbSkipAllInThisPass = false;
sal_Int32 mnCurrentPass = 0;
/// increments index depending on the current render stage
void incrementIndex()
{
if (meStage == RenderStage::Master)
mnMasterIndex++;
else
mnIndex++;
}
/// returns the current stage as string
OString stageString()
{
if (meStage == RenderStage::Master)
return "MasterPage"_ostr;
return "DrawPage"_ostr;
}
/// returns the current index depending on the current render stage
sal_Int32 currentIndex()
{
if (meStage == RenderStage::Master)
return mnMasterIndex;
return mnIndex;
}
/// resets properties that are valid for one pass
void resetPass()
{
mbFirstObjectInPass = true;
mbPassHasOutput = false;
mbSkipAllInThisPass = false;
}
/// return if there was no rendering output in the pass
bool noMoreOutput()
{
// no output and we don't skip anything
return !mbPassHasOutput && !mbSkipAllInThisPass;
}
/// should include background in rendering
bool includeBackground()
{
// include background only if we are rendering the first pass
return mnCurrentPass == 0;
}
bool isObjectAlreadyRendered(SdrObject* pObject)
{
return maObjectsDone.find(pObject) != maObjectsDone.end();
}
};
/** Renders a slide */
class SD_DLLPUBLIC SlideshowLayerRenderer class SD_DLLPUBLIC SlideshowLayerRenderer
{ {
private:
SdrPage& mrPage; SdrPage& mrPage;
SdrModel& mrModel; SdrModel& mrModel;
Size maSlideSize; Size maSlideSize;
RenderState maRenderState;
std::deque<SlideRenderStage> maRenderStages; void createViewAndDraw(RenderContext& rRenderContext);
void writeJSON(OString& rJsonMsg);
void cleanupRendering(RenderContext& rRenderContext);
void setupRendering(unsigned char* pBuffer, RenderContext& rRenderContext);
void createViewAndDraw(RenderContext& rRenderContext, RenderOptions const& rRenderOptions);
public: public:
SlideshowLayerRenderer(SdrPage& rPage); SlideshowLayerRenderer(SdrPage& rPage);
/** Calculate and set the slide size depending on input desired size (in pixels)
*
* Input the desired size in pixels, and the actual size pixels will be caluclated
* depending on the size of the slide and the desired size. The size can differ,
* because the it must match the slide aspect ratio.
**/
Size calculateAndSetSizePixel(Size const& rDesiredSizePixel); Size calculateAndSetSizePixel(Size const& rDesiredSizePixel);
/** Renders one layer
*
* The slide layer is rendered into the input buffer, which must be the byte size
* of the calcualted size in pixels * 4 (RGBA).
* The properties of the layer are written to the input string in JSON format.
*
* @returns false, if nothing was rendered and rendering is done */
bool render(unsigned char* pBuffer, OString& rJsonMsg); bool render(unsigned char* pBuffer, OString& rJsonMsg);
bool renderMaster(unsigned char* pBuffer, OString& rJsonMsg);
bool renderSlide(unsigned char* pBuffer, OString& rJsonMsg);
}; };
} // end of namespace sd } // end of namespace sd

View File

@ -24,13 +24,6 @@
namespace sd namespace sd
{ {
struct RenderOptions
{
bool mbIncludeBackground = true;
bool mbSkipMainPageObjects = false;
bool mbSkipMasterPageObjects = false;
};
struct RenderContext struct RenderContext
{ {
SdrModel& mrModel; SdrModel& mrModel;
@ -44,7 +37,7 @@ struct RenderContext
, mrPage(rPage) , mrPage(rPage)
, maVirtualDevice(DeviceFormat::WITHOUT_ALPHA) , maVirtualDevice(DeviceFormat::WITHOUT_ALPHA)
{ {
// Turn of spelling // Turn off spelling
SdrOutliner& rOutliner = mrModel.GetDrawOutliner(); SdrOutliner& rOutliner = mrModel.GetDrawOutliner();
mnSavedControlBits = rOutliner.GetControlWord(); mnSavedControlBits = rOutliner.GetControlWord();
rOutliner.SetControlWord(mnSavedControlBits & ~EEControlBits::ONLINESPELLING); rOutliner.SetControlWord(mnSavedControlBits & ~EEControlBits::ONLINESPELLING);
@ -77,14 +70,31 @@ struct RenderContext
namespace namespace
{ {
bool hasFields(SdrObject* pObject)
{
auto* pTextObject = dynamic_cast<SdrTextObj*>(pObject);
if (!pTextObject)
return false;
OutlinerParaObject* pOutlinerParagraphObject = pTextObject->GetOutlinerParaObject();
if (pOutlinerParagraphObject)
{
const EditTextObject& rEditText = pOutlinerParagraphObject->GetTextObject();
if (rEditText.IsFieldObject())
return true;
}
return false;
}
/** VOC redirector to control which object should be rendered and which not */
class ObjectRedirector : public sdr::contact::ViewObjectContactRedirector class ObjectRedirector : public sdr::contact::ViewObjectContactRedirector
{ {
protected: protected:
RenderOptions maOptions; RenderState& mrRenderState;
public: public:
ObjectRedirector(RenderOptions const& rOptions) ObjectRedirector(RenderState& rRenderState)
: maOptions(rOptions) : mrRenderState(rRenderState)
{ {
} }
@ -93,36 +103,62 @@ public:
const sdr::contact::DisplayInfo& rDisplayInfo, const sdr::contact::DisplayInfo& rDisplayInfo,
drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) override drawinglayer::primitive2d::Primitive2DDecompositionVisitor& rVisitor) override
{ {
if (mrRenderState.mbSkipAllInThisPass)
return;
SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject(); SdrObject* pObject = rOriginal.GetViewContact().TryToGetSdrObject();
SdrPage* pPage = pObject ? pObject->getSdrPageFromSdrObject() : nullptr; // Check if we are rendering an object that is valid to render (exists, and not empty)
if (pObject == nullptr || pObject->IsEmptyPresObj())
return;
if (pObject == nullptr || pPage == nullptr) SdrPage* pPage = pObject->getSdrPageFromSdrObject();
// Does the object have a page
if (pPage == nullptr)
return;
// is the object visible and not hidden by any option
const bool bVisible
= pObject->getSdrPageFromSdrObject()->checkVisibility(rOriginal, rDisplayInfo, true);
if (!bVisible)
return;
// Check if we have already rendered the object
if (mrRenderState.isObjectAlreadyRendered(pObject))
return;
// Check if we are in correct stage
if (mrRenderState.meStage == RenderStage::Master && !pPage->IsMasterPage())
{ {
// Not a SdrObject or a object not connected to a page (object with no page) if (mrRenderState.mbFirstObjectInPass)
{
// 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;
}
}
if (mrRenderState.meStage == RenderStage::Master && hasFields(pObject)
&& mrRenderState.mbStopRenderingWhenField && !mrRenderState.mbFirstObjectInPass)
{
mrRenderState.mbStopRenderingWhenField = false;
mrRenderState.mbSkipAllInThisPass = true;
return; return;
} }
if (maOptions.mbSkipMasterPageObjects && pPage->IsMasterPage()) // render the object
return;
if (maOptions.mbSkipMainPageObjects && !pPage->IsMasterPage())
return;
const bool bDoCreateGeometry(
pObject->getSdrPageFromSdrObject()->checkVisibility(rOriginal, rDisplayInfo, true));
if (!bDoCreateGeometry
&& (pObject->GetObjInventor() != SdrInventor::Default
|| pObject->GetObjIdentifier() != SdrObjKind::Page))
{
return;
}
if (pObject->IsEmptyPresObj())
return;
sdr::contact::ViewObjectContactRedirector::createRedirectedPrimitive2DSequence( sdr::contact::ViewObjectContactRedirector::createRedirectedPrimitive2DSequence(
rOriginal, rDisplayInfo, rVisitor); rOriginal, rDisplayInfo, rVisitor);
mrRenderState.mbFirstObjectInPass = false;
mrRenderState.maObjectsDone.insert(pObject);
mrRenderState.mbPassHasOutput = true;
} }
}; };
@ -148,8 +184,9 @@ SlideshowLayerRenderer::SlideshowLayerRenderer(SdrPage& rPage)
, mrModel(rPage.getSdrModelFromSdrPage()) , mrModel(rPage.getSdrModelFromSdrPage())
{ {
if (!hasEmptyMaster(rPage)) if (!hasEmptyMaster(rPage))
maRenderStages.emplace_back(SlideRenderStage::Master); maRenderState.meStage = RenderStage::Master;
maRenderStages.emplace_back(SlideRenderStage::Slide); else
maRenderState.meStage = RenderStage::Slide;
} }
Size SlideshowLayerRenderer::calculateAndSetSizePixel(Size const& rDesiredSizePixel) Size SlideshowLayerRenderer::calculateAndSetSizePixel(Size const& rDesiredSizePixel)
@ -161,8 +198,7 @@ Size SlideshowLayerRenderer::calculateAndSetSizePixel(Size const& rDesiredSizePi
return maSlideSize; return maSlideSize;
} }
void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext, void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext)
RenderOptions const& rRenderOptions)
{ {
SdrView aView(mrModel, rRenderContext.maVirtualDevice); SdrView aView(mrModel, rRenderContext.maVirtualDevice);
aView.SetPageVisible(false); aView.SetPageVisible(false);
@ -172,29 +208,24 @@ void SlideshowLayerRenderer::createViewAndDraw(RenderContext& rRenderContext,
aView.SetGridVisible(false); aView.SetGridVisible(false);
aView.SetHlplVisible(false); aView.SetHlplVisible(false);
aView.SetGlueVisible(false); aView.SetGlueVisible(false);
aView.setHideBackground(!rRenderOptions.mbIncludeBackground); aView.setHideBackground(!maRenderState.includeBackground());
aView.ShowSdrPage(&mrPage); aView.ShowSdrPage(&mrPage);
Size aPageSize(mrPage.GetSize()); Size aPageSize(mrPage.GetSize());
Point aPoint; Point aPoint;
vcl::Region aRegion(::tools::Rectangle(aPoint, aPageSize)); vcl::Region aRegion(::tools::Rectangle(aPoint, aPageSize));
ObjectRedirector aRedirector(rRenderOptions); ObjectRedirector aRedirector(maRenderState);
aView.CompleteRedraw(rRenderContext.maVirtualDevice, aRegion, &aRedirector); aView.CompleteRedraw(rRenderContext.maVirtualDevice, aRegion, &aRedirector);
} }
bool SlideshowLayerRenderer::renderMaster(unsigned char* pBuffer, OString& rJsonMsg) void SlideshowLayerRenderer::writeJSON(OString& rJsonMsg)
{ {
RenderOptions aRenderOptions;
aRenderOptions.mbSkipMainPageObjects = true;
RenderContext aRenderContext(pBuffer, mrModel, mrPage, maSlideSize);
createViewAndDraw(aRenderContext, aRenderOptions);
::tools::JsonWriter aJsonWriter; ::tools::JsonWriter aJsonWriter;
aJsonWriter.put("group", "MasterPage"); aJsonWriter.put("group", maRenderState.stageString());
aJsonWriter.put("index", maRenderState.currentIndex());
aJsonWriter.put("slideHash", GetInterfaceHash(GetXDrawPageForSdrPage(&mrPage))); aJsonWriter.put("slideHash", GetInterfaceHash(GetXDrawPageForSdrPage(&mrPage)));
aJsonWriter.put("index", 0);
aJsonWriter.put("type", "bitmap"); aJsonWriter.put("type", "bitmap");
{ {
auto aContentNode = aJsonWriter.startNode("content"); auto aContentNode = aJsonWriter.startNode("content");
@ -203,47 +234,24 @@ bool SlideshowLayerRenderer::renderMaster(unsigned char* pBuffer, OString& rJson
} }
rJsonMsg = aJsonWriter.finishAndGetAsOString(); rJsonMsg = aJsonWriter.finishAndGetAsOString();
return true; maRenderState.incrementIndex();
}
bool SlideshowLayerRenderer::renderSlide(unsigned char* pBuffer, OString& rJsonMsg)
{
RenderOptions aRenderOptions;
aRenderOptions.mbSkipMasterPageObjects = true;
RenderContext aRenderContext(pBuffer, mrModel, mrPage, maSlideSize);
createViewAndDraw(aRenderContext, aRenderOptions);
::tools::JsonWriter aJsonWriter;
aJsonWriter.put("group", "DrawPage");
aJsonWriter.put("slideHash", GetInterfaceHash(GetXDrawPageForSdrPage(&mrPage)));
aJsonWriter.put("index", 0);
aJsonWriter.put("type", "bitmap");
{
auto aContentNode = aJsonWriter.startNode("content");
aJsonWriter.put("type", "%IMAGETYPE%");
aJsonWriter.put("checksum", "%IMAGECHECKSUM%");
}
rJsonMsg = aJsonWriter.finishAndGetAsOString();
return true;
} }
bool SlideshowLayerRenderer::render(unsigned char* pBuffer, OString& rJsonMsg) bool SlideshowLayerRenderer::render(unsigned char* pBuffer, OString& rJsonMsg)
{ {
if (maRenderStages.empty()) // Reset state
maRenderState.resetPass();
RenderContext aRenderContext(pBuffer, mrModel, mrPage, maSlideSize);
createViewAndDraw(aRenderContext);
// Check if we are done rendering all passes
if (maRenderState.noMoreOutput())
return false; return false;
auto eRenderStage = maRenderStages.front(); writeJSON(rJsonMsg);
maRenderStages.pop_front();
switch (eRenderStage) maRenderState.mnCurrentPass++;
{
case SlideRenderStage::Master:
return renderMaster(pBuffer, rJsonMsg);
case SlideRenderStage::Slide:
return renderSlide(pBuffer, rJsonMsg);
};
return true; return true;
} }