From fd02bc931f87c971801d65380bfe08cedf202889 Mon Sep 17 00:00:00 2001 From: Andras Timar Date: Sun, 14 Jun 2026 23:16:18 +0200 Subject: [PATCH] oox: export custom-shape equations and handles to DrawingML custGeom A custom shape that has no matching OOXML preset is exported as . Until now that path baked every coordinate to a fixed number and wrote empty and , so the shape lost its adjustment values and adjust handles: it became frozen and could no longer be edited by its control points after saving to docx/pptx/xlsx. Convert the shape's ODF "Equations" to DrawingML guide formulas by walking the parsed expression tree (FunctionParser::parseFunction). At that level the original structure is intact, which lets the export: - emit guides for the equations, keeping width/height symbolic ("w"/"h") so the geometry scales correctly on resize; - express the trigonometric functions (sin/cos/tan): the angle-conversion idiom (X*pi/180 or pi*X/10800000) is recognised and reconciled to OOXML's 60000ths of a degree, for both trig arguments and standalone angle values such as arc parameters; - declare the referenced adjustment values in ("adj1", ...) so the guides stay driven by the shape's handles; - emit an handle for each ODF handle that drives an adjustment value, referencing the matching adjustment guide; - reference an equation from a path coordinate (or handle) by the token that holds its value. svx gains two small ExpressionNode accessors (getChild/getIndex) so the expression tree can be walked from the export filter. Equations or handles that cannot be expressed yet (raw-radians angles without the pi/const idiom, atan2/cat2/sat2, ellipse, polar handles) fall back to resolved coordinates, as before. Also update the now-outdated comment in ShapeExport that claimed ODF equations cannot be converted to DrawingML. Adds a test on a symbol shape (tdf#142603) and a circular arrow (tdf#157289 trig guides), and updates testFlipAndRotateCustomShape / testEquationGuidesExport for the new parametric output. Signed-off-by: Andras Timar Change-Id: I21cf73df018417fcfe9c2f9ecd185c0ac29a198d Reviewed-on: https://gerrit.collaboraoffice.com/c/online/+/4464 Tested-by: Jenkins CPCI Reviewed-by: Miklos Vajna (cherry picked from commit 40967878aeaee989216b6e0cfd6cf81af4a8b1d8) Reviewed-on: https://gerrit.libreoffice.org/c/core/+/206685 Tested-by: Jenkins Reviewed-by: Xisco Fauli --- include/oox/export/drawingml.hxx | 6 +- .../svx/EnhancedCustomShapeFunctionParser.hxx | 12 + oox/qa/unit/data/tdf142603.odt | Bin 0 -> 10061 bytes oox/qa/unit/export.cxx | 31 ++ oox/source/export/drawingml.cxx | 505 +++++++++++++++++- oox/source/export/shapes.cxx | 6 +- sd/qa/unit/export-tests-ooxml3.cxx | 18 + .../EnhancedCustomShapeFunctionParser.cxx | 24 + sw/qa/extras/ooxmlexport/ooxmlexport7.cxx | 23 +- 9 files changed, 591 insertions(+), 34 deletions(-) create mode 100644 oox/qa/unit/data/tdf142603.odt diff --git a/include/oox/export/drawingml.hxx b/include/oox/export/drawingml.hxx index 63fe22c3186b..c26f28a97110 100644 --- a/include/oox/export/drawingml.hxx +++ b/include/oox/export/drawingml.hxx @@ -365,13 +365,15 @@ protected: void WriteSoftEdgeEffect(const css::uno::Reference& rXPropSet); void WriteCustomGeometryPoint(const css::drawing::EnhancedCustomShapeParameterPair& rParamPair, const EnhancedCustomShape2d& rCustomShape2d, - const bool bReplaceGeoWidth, const bool bReplaceGeoHeight); + const bool bReplaceGeoWidth, const bool bReplaceGeoHeight, + const std::vector* pCoordGuides = nullptr); bool WriteCustomGeometrySegment( const sal_Int16 eCommand, const sal_Int32 nCount, const css::uno::Sequence& rPairs, sal_Int32& rnPairIndex, double& rfCurrentX, double& rfCurrentY, bool& rbCurrentValid, const EnhancedCustomShape2d& rCustomShape2d, - const bool bReplaceGeoWidth, const bool bReplaceGeoHeight); + const bool bReplaceGeoWidth, const bool bReplaceGeoHeight, + const std::vector* pCoordGuides = nullptr); public: OOX_DLLPUBLIC DrawingML(::sax_fastparser::FSHelperPtr pFS, ::oox::core::XmlFilterBase* pFB, DocumentType eDocumentType = DOCUMENT_PPTX, DMLTextExport* pTextExport = nullptr); diff --git a/include/svx/EnhancedCustomShapeFunctionParser.hxx b/include/svx/EnhancedCustomShapeFunctionParser.hxx index 28d2491c77f8..4b2a4049bdb2 100644 --- a/include/svx/EnhancedCustomShapeFunctionParser.hxx +++ b/include/svx/EnhancedCustomShapeFunctionParser.hxx @@ -162,6 +162,18 @@ public: */ virtual ExpressionFunct getType() const = 0; + /** Child sub-expression by index (0-based), or nullptr if out of range. + + Lets a caller walk the expression tree: unary nodes have child 0, binary + nodes children 0 and 1, the ternary "if" node children 0..2. Leaf nodes + (constants, adjustment/equation/enum references) have no children. + */ + virtual const ExpressionNode* getChild(sal_Int32 /*nIndex*/) const { return nullptr; } + + /** Index of an adjustment ($n) or equation (?fn) reference, else -1. + */ + virtual sal_Int32 getIndex() const { return -1; } + /** Operator to retrieve the ms version of expression */ virtual css::drawing::EnhancedCustomShapeParameter fillNode( diff --git a/oox/qa/unit/data/tdf142603.odt b/oox/qa/unit/data/tdf142603.odt new file mode 100644 index 0000000000000000000000000000000000000000..187f646b6f40658267e2cded1aa90ab35ffa2124 GIT binary patch literal 10061 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 must keep its + // adjustment handle so it stays editable. The handle () references an + // adjustment guide declared in , which the geometry formulas + // () use in turn. Without the fix and were empty + // and the shape was frozen after export. + loadFromFile(u"tdf142603.odt"); + save(TestFilter::DOCX); + xmlDocUniquePtr pXmlDoc = parseExport(u"word/document.xml"_ustr); + + // Pick the shape (a "moon") whose handle drives adjustment adj1 in X. + OString aShape + = "(//wps:wsp[.//a:custGeom/a:ahLst/a:ahXY/@gdRefX])[1]/wps:spPr/a:custGeom"_ostr; + // The adjustment value is declared in the adjust-value list. + assertXPath(pXmlDoc, aShape + "/a:avLst/a:gd[@name='adj1']", 1); + // A geometry guide uses it (so changing it re-evaluates the geometry). + assertXPath(pXmlDoc, aShape + "/a:gdLst/a:gd[contains(@fmla,'adj1')][1]", 1); + // The handle drives adj1 and is positioned at it. + assertXPath(pXmlDoc, aShape + "/a:ahLst/a:ahXY", "gdRefX", u"adj1"); + assertXPath(pXmlDoc, aShape + "/a:ahLst/a:ahXY/a:pos", "x", u"adj1"); +} + CPPUNIT_TEST_FIXTURE(Test, testCustomShapeArrowExport) { // Given a document with a few different kinds of arrow shapes in it: @@ -1326,6 +1349,14 @@ CPPUNIT_TEST_FIXTURE(Test, testTdf157289CircularArrowExport) xmlDocUniquePtr pXmlDoc = parseExport(u"ppt/slides/slide1.xml"_ustr); assertXPath(pXmlDoc, "//a:pathLst/a:path/a:arcTo[1]", "wR", u"6750"); assertXPath(pXmlDoc, "//a:pathLst/a:path/a:arcTo[1]", "hR", u"6750"); + + // The shape's trigonometric equations are exported as guides: the angle + // adjustment (degrees) is converted to OOXML's 60000ths of a degree (x*10800000/180) + // and then used by a sin/cos guide. + OString aGdLst = "//a:custGeom/a:gdLst"_ostr; + CPPUNIT_ASSERT(countXPathNodes(pXmlDoc, aGdLst + "/a:gd[@fmla='*/ adj1 10800000 180']") > 0); + CPPUNIT_ASSERT(countXPathNodes(pXmlDoc, aGdLst + "/a:gd[starts-with(@fmla,'sin ')]") > 0); + CPPUNIT_ASSERT(countXPathNodes(pXmlDoc, aGdLst + "/a:gd[starts-with(@fmla,'cos ')]") > 0); } CPPUNIT_TEST_FIXTURE(Test, testTdf134401_ExportAutoGrowToTextWordWrap) diff --git a/oox/source/export/drawingml.cxx b/oox/source/export/drawingml.cxx index 724658bbe4b7..bd51c3727a92 100644 --- a/oox/source/export/drawingml.cxx +++ b/oox/source/export/drawingml.cxx @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -58,6 +59,7 @@ #include #include #include +#include #include #include #include @@ -137,6 +139,7 @@ #include #include #include +#include #include #include #include @@ -5065,6 +5068,416 @@ OUString GetFormula(const OUString& sEquation) return OUString(); } +// Convert a custom shape's ODF equations directly from the parsed expression tree +// into OOXML guide formulas. Working from the expression tree (rather than the +// flattened binary form the DOC filter uses) keeps width/height symbolic ("w"/"h") +// and lets us express the trigonometric functions, because the angle-conversion +// idiom is still visible; the angle unit is reconciled to OOXML's 60000ths of a +// degree. +using EnhancedCustomShape::ExpressionNode; +using ExpressionFunct = EnhancedCustomShape::ExpressionFunct; + +// Carries the guides built so far while one expression tree is walked. +struct OoxmlEqContext +{ + std::vector& rGuides; // guides, in dependency order + std::set& rUsedAdjustments; // adjustment indices referenced + const std::vector& rEqNames; // source equation index -> its OOXML token + const std::vector& rEqIsAngle; // source equation index -> token is an angle (60000ths) + bool bFailed = false; + + OString addGuide(const OString& rFormula) + { + OString aName = "f" + OString::number(rGuides.size()); + rGuides.push_back({ aName, rFormula }); + return aName; + } +}; + +OString lcl_EmitOoxmlNode(const ExpressionNode* pNode, OoxmlEqContext& rCtx); + +// Is p the sub-expression "pi / "? Returns the constant in rConst. +bool lcl_IsPiOverConst(const ExpressionNode* p, double& rConst) +{ + if (p && p->getType() == ExpressionFunct::BinaryDiv) + { + const ExpressionNode* pNum = p->getChild(0); + const ExpressionNode* pDen = p->getChild(1); + if (pNum && pDen && pNum->getType() == ExpressionFunct::EnumPi && pDen->isConstant()) + { + rConst = (*pDen)(); + return true; + } + } + return false; +} + +// If pArg is a radians angle of the form X*(pi/c) or (pi*X)/c, emit it converted +// to OOXML 60000ths of a degree (X*10800000/c) and return true; else return false +// (leaving rCtx untouched). This is the only way to express an angle in OOXML, +// whose guide formulas have no pi constant to cancel against. Used both for +// standalone angle values (e.g. arc parameters) and trigonometric arguments. +bool lcl_TryEmitAngle60000(const ExpressionNode* pArg, OoxmlEqContext& rCtx, OString& rResult) +{ + // A reference to an equation that is itself an angle is already in 60000ths. + if (pArg && pArg->getType() == ExpressionFunct::EnumEquation) + { + const sal_Int32 nEquation = pArg->getIndex(); + if (nEquation >= 0 && o3tl::make_unsigned(nEquation) < rCtx.rEqIsAngle.size() + && rCtx.rEqIsAngle[nEquation] && !rCtx.rEqNames[nEquation].isEmpty()) + { + rResult = rCtx.rEqNames[nEquation]; + return true; + } + return false; + } + const ExpressionNode* pX = nullptr; + double fConst = 0.0; + if (pArg && pArg->getType() == ExpressionFunct::BinaryMul) + { + if (lcl_IsPiOverConst(pArg->getChild(0), fConst)) + pX = pArg->getChild(1); + else if (lcl_IsPiOverConst(pArg->getChild(1), fConst)) + pX = pArg->getChild(0); + } + else if (pArg && pArg->getType() == ExpressionFunct::BinaryDiv) + { + const ExpressionNode* pNum = pArg->getChild(0); + const ExpressionNode* pDen = pArg->getChild(1); + if (pNum && pDen && pDen->isConstant() && pNum->getType() == ExpressionFunct::BinaryMul) + { + const ExpressionNode* pA = pNum->getChild(0); + const ExpressionNode* pB = pNum->getChild(1); + if (pA && pA->getType() == ExpressionFunct::EnumPi) + pX = pB; + else if (pB && pB->getType() == ExpressionFunct::EnumPi) + pX = pA; + if (pX) + fConst = (*pDen)(); + } + } + if (!pX || fConst == 0.0) + return false; + OString aX = lcl_EmitOoxmlNode(pX, rCtx); + rResult = rCtx.addGuide("*/ " + aX + " 10800000 " + + OString::number(static_cast(std::lround(fConst)))); + return true; +} + +bool lcl_IsTrig(const ExpressionNode* p) +{ + const ExpressionFunct e = p->getType(); + return e == ExpressionFunct::UnarySin || e == ExpressionFunct::UnaryCos + || e == ExpressionFunct::UnaryTan; +} + +// Emit "sin|cos|tan "; pMul is the multiplier in "mul*sin(x)", or null (1). +OString lcl_EmitTrig(const ExpressionNode* pTrig, const ExpressionNode* pMul, OoxmlEqContext& rCtx) +{ + OString aAngle; + if (!lcl_TryEmitAngle60000(pTrig->getChild(0), rCtx, aAngle)) + { + // Angle not in the pi/const idiom (e.g. raw radians) - not expressible. + rCtx.bFailed = true; + return "0"_ostr; + } + const char* pOp = pTrig->getType() == ExpressionFunct::UnarySin + ? "sin" + : (pTrig->getType() == ExpressionFunct::UnaryCos ? "cos" : "tan"); + OString aMul = pMul ? lcl_EmitOoxmlNode(pMul, rCtx) : "1"_ostr; + return rCtx.addGuide(OString::Concat(pOp) + " " + aMul + " " + aAngle); +} + +OString lcl_EmitOoxmlNode(const ExpressionNode* pNode, OoxmlEqContext& rCtx) +{ + if (!pNode) + { + rCtx.bFailed = true; + return "0"_ostr; + } + // A constant sub-expression collapses to its literal value. + if (pNode->isConstant()) + return OString::number(static_cast(std::lround((*pNode)()))); + + switch (pNode->getType()) + { + case ExpressionFunct::EnumAdjustment: + { + const sal_Int32 nAdjustment = pNode->getIndex(); + rCtx.rUsedAdjustments.insert(nAdjustment); + return "adj" + OString::number(nAdjustment + 1); + } + case ExpressionFunct::EnumEquation: + { + const sal_Int32 nEquation = pNode->getIndex(); + if (nEquation >= 0 && o3tl::make_unsigned(nEquation) < rCtx.rEqNames.size() + && !rCtx.rEqNames[nEquation].isEmpty()) + return rCtx.rEqNames[nEquation]; + rCtx.bFailed = true; + return "0"_ostr; + } + case ExpressionFunct::EnumWidth: + case ExpressionFunct::EnumLogWidth: + return "w"_ostr; + case ExpressionFunct::EnumHeight: + case ExpressionFunct::EnumLogHeight: + return "h"_ostr; + case ExpressionFunct::EnumLeft: + return "l"_ostr; + case ExpressionFunct::EnumTop: + return "t"_ostr; + case ExpressionFunct::EnumRight: + return "r"_ostr; + case ExpressionFunct::EnumBottom: + return "b"_ostr; + case ExpressionFunct::BinaryPlus: + return rCtx.addGuide("+- " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " " + + lcl_EmitOoxmlNode(pNode->getChild(1), rCtx) + " 0"); + case ExpressionFunct::BinaryMinus: + return rCtx.addGuide("+- " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " 0 " + + lcl_EmitOoxmlNode(pNode->getChild(1), rCtx)); + case ExpressionFunct::BinaryMul: + { + OString aAngle; + if (lcl_TryEmitAngle60000(pNode, rCtx, aAngle)) + return aAngle; // X*(pi/c) used as a standalone angle value + const ExpressionNode* pA = pNode->getChild(0); + const ExpressionNode* pB = pNode->getChild(1); + if (lcl_IsTrig(pA)) + return lcl_EmitTrig(pA, pB, rCtx); + if (lcl_IsTrig(pB)) + return lcl_EmitTrig(pB, pA, rCtx); + return rCtx.addGuide("*/ " + lcl_EmitOoxmlNode(pA, rCtx) + " " + + lcl_EmitOoxmlNode(pB, rCtx) + " 1"); + } + case ExpressionFunct::BinaryDiv: + { + OString aAngle; + if (lcl_TryEmitAngle60000(pNode, rCtx, aAngle)) + return aAngle; // (pi*X)/c used as a standalone angle value + return rCtx.addGuide("*/ " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " 1 " + + lcl_EmitOoxmlNode(pNode->getChild(1), rCtx)); + } + case ExpressionFunct::BinaryMin: + return rCtx.addGuide("min " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " " + + lcl_EmitOoxmlNode(pNode->getChild(1), rCtx)); + case ExpressionFunct::BinaryMax: + return rCtx.addGuide("max " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " " + + lcl_EmitOoxmlNode(pNode->getChild(1), rCtx)); + case ExpressionFunct::UnaryNeg: + return rCtx.addGuide("*/ " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " -1 1"); + case ExpressionFunct::UnaryAbs: + return rCtx.addGuide("abs " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx)); + case ExpressionFunct::UnarySqrt: + return rCtx.addGuide("sqrt " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx)); + case ExpressionFunct::TernaryIf: + return rCtx.addGuide("?: " + lcl_EmitOoxmlNode(pNode->getChild(0), rCtx) + " " + + lcl_EmitOoxmlNode(pNode->getChild(1), rCtx) + " " + + lcl_EmitOoxmlNode(pNode->getChild(2), rCtx)); + case ExpressionFunct::UnarySin: + case ExpressionFunct::UnaryCos: + case ExpressionFunct::UnaryTan: + return lcl_EmitTrig(pNode, nullptr, rCtx); + default: + // atan2/cat2/sat2, bare pi, stretch/has-fill flags: not expressible yet. + rCtx.bFailed = true; + return "0"_ostr; + } +} + +// Convert all of a shape's ODF "Equations" to OOXML guides. On success rGuides +// holds the guides, rUsedAdjustments the adjustments for , and +// rEqNames maps each source equation to the OOXML token (guide name or literal) +// that holds its value. Returns false if any equation uses a construct we cannot +// express, so the caller can fall back to resolved coordinates. +bool lcl_CreateOoxmlGuides(const EnhancedCustomShape2d& rCustomShape2d, + const css::uno::Sequence& rEquationStrings, + std::vector& rGuides, std::set& rUsedAdjustments, + std::vector& rEqNames) +{ + rEqNames.assign(rEquationStrings.getLength(), OString()); + std::vector aEqIsAngle(rEquationStrings.getLength(), false); + OoxmlEqContext aContext{ rGuides, rUsedAdjustments, rEqNames, aEqIsAngle }; + for (sal_Int32 i = 0; i < rEquationStrings.getLength(); ++i) + { + std::shared_ptr pNode; + try + { + pNode = EnhancedCustomShape::FunctionParser::parseFunction(rEquationStrings[i], + rCustomShape2d); + } + catch (...) + { + return false; + } + aContext.bFailed = false; + // An equation that is itself an angle (X*pi/c) is converted to 60000ths of + // a degree and remembered, so trig functions referencing it work. + OString aToken; + if (lcl_TryEmitAngle60000(pNode.get(), aContext, aToken)) + aEqIsAngle[i] = true; + else + aToken = lcl_EmitOoxmlNode(pNode.get(), aContext); + if (aContext.bFailed) + return false; + rEqNames[i] = std::move(aToken); + } + return true; +} + +// Format a single EnhancedCustomShapeParameter (a handle position or range +// value) as an OOXML adjust-handle token: an adjustment as "adj", an equation +// result as its guide "f", the built-in box edges as l/t/r/b, anything else +// as its literal value. +OString lcl_GetHandleValue(const css::drawing::EnhancedCustomShapeParameter& rParam, + const std::vector* pEqNames) +{ + sal_Int32 nValue = 0; + if (double fValue = 0.0; rParam.Value >>= fValue) + nValue = std::lround(fValue); + else + rParam.Value >>= nValue; + + switch (rParam.Type) + { + case css::drawing::EnhancedCustomShapeParameterType::ADJUSTMENT: + return "adj" + OString::number(nValue + 1); + case css::drawing::EnhancedCustomShapeParameterType::EQUATION: + if (pEqNames && nValue >= 0 && o3tl::make_unsigned(nValue) < pEqNames->size() + && !(*pEqNames)[nValue].isEmpty()) + return (*pEqNames)[nValue]; + return OString::number(nValue); + case css::drawing::EnhancedCustomShapeParameterType::LEFT: + return "l"_ostr; + case css::drawing::EnhancedCustomShapeParameterType::TOP: + return "t"_ostr; + case css::drawing::EnhancedCustomShapeParameterType::RIGHT: + return "r"_ostr; + case css::drawing::EnhancedCustomShapeParameterType::BOTTOM: + return "b"_ostr; + default: + return OString::number(nValue); + } +} + +// Write the adjust handles. An ODF handle whose position drives an +// adjustment value becomes an handle that references the matching +// adjustment guide ("adj"), so the handle stays draggable after export. Only +// adjustments declared in (rUsedAdjustments) are referenced; polar +// handles are not converted yet. +void lcl_WriteAdjustHandles(const sax_fastparser::FSHelperPtr& rFS, + const uno::Sequence& rHandles, + const std::vector* pEqNames, + const std::set& rUsedAdjustments) +{ + struct XYHandle + { + OString aPosX, aPosY, aGdRefX, aMinX, aMaxX, aGdRefY, aMinY, aMaxY; + }; + auto getAdjustment + = [&rUsedAdjustments](const css::drawing::EnhancedCustomShapeParameter& rParam, + sal_Int32& rIndex) { + if (rParam.Type != css::drawing::EnhancedCustomShapeParameterType::ADJUSTMENT) + return false; + rIndex = -1; + rParam.Value >>= rIndex; + return rIndex >= 0 && rUsedAdjustments.count(rIndex) > 0; + }; + + std::vector aHandlesOut; + for (const beans::PropertyValues& rHandle : rHandles) + { + comphelper::SequenceAsHashMap aMap(rHandle); + if (aMap.find(u"Polar"_ustr) != aMap.end()) + continue; // polar handles are not converted yet + + css::drawing::EnhancedCustomShapeParameterPair aPosition; + auto itPos = aMap.find(u"Position"_ustr); + if (itPos == aMap.end() || !(itPos->second >>= aPosition)) + continue; + + XYHandle aOut; + aOut.aPosX = lcl_GetHandleValue(aPosition.First, pEqNames); + aOut.aPosY = lcl_GetHandleValue(aPosition.Second, pEqNames); + + auto readRange = [&aMap, pEqNames](std::u16string_view rName, OString& rOut) { + css::drawing::EnhancedCustomShapeParameter aParam; + if (auto it = aMap.find(OUString(rName)); it != aMap.end() && (it->second >>= aParam)) + rOut = lcl_GetHandleValue(aParam, pEqNames); + }; + + sal_Int32 nAdjustment = -1; + if (getAdjustment(aPosition.First, nAdjustment)) + { + aOut.aGdRefX = "adj" + OString::number(nAdjustment + 1); + readRange(u"RangeXMinimum", aOut.aMinX); + readRange(u"RangeXMaximum", aOut.aMaxX); + } + if (getAdjustment(aPosition.Second, nAdjustment)) + { + aOut.aGdRefY = "adj" + OString::number(nAdjustment + 1); + readRange(u"RangeYMinimum", aOut.aMinY); + readRange(u"RangeYMaximum", aOut.aMaxY); + } + + // Only a handle that drives an adjustment value is useful. + if (!aOut.aGdRefX.isEmpty() || !aOut.aGdRefY.isEmpty()) + aHandlesOut.push_back(aOut); + } + + if (aHandlesOut.empty()) + { + rFS->singleElementNS(XML_a, XML_ahLst); + return; + } + + rFS->startElementNS(XML_a, XML_ahLst); + for (const XYHandle& rHandle : aHandlesOut) + { + rtl::Reference xAttrs + = sax_fastparser::FastSerializerHelper::createAttrList(); + if (!rHandle.aGdRefX.isEmpty()) + xAttrs->add(XML_gdRefX, rHandle.aGdRefX); + if (!rHandle.aMinX.isEmpty()) + xAttrs->add(XML_minX, rHandle.aMinX); + if (!rHandle.aMaxX.isEmpty()) + xAttrs->add(XML_maxX, rHandle.aMaxX); + if (!rHandle.aGdRefY.isEmpty()) + xAttrs->add(XML_gdRefY, rHandle.aGdRefY); + if (!rHandle.aMinY.isEmpty()) + xAttrs->add(XML_minY, rHandle.aMinY); + if (!rHandle.aMaxY.isEmpty()) + xAttrs->add(XML_maxY, rHandle.aMaxY); + rFS->startElementNS(XML_a, XML_ahXY, xAttrs); + rFS->singleElementNS(XML_a, XML_pos, XML_x, rHandle.aPosX, XML_y, rHandle.aPosY); + rFS->endElementNS(XML_a, XML_ahXY); + } + rFS->endElementNS(XML_a, XML_ahLst); +} + +// Format one path coordinate. When the coordinate is an equation, pCoordGuides +// gives the token holding its value - a guide name ("f") for a parametric +// equation, or a plain number for a constant one - so the point follows the +// guides. A stretched coordinate (StretchX/StretchY) keeps its resolved value, +// because the guide does not carry the stretch adjustment. +OString lcl_GetCustomGeometryCoordinate(const drawing::EnhancedCustomShapeParameter& rParam, + const EnhancedCustomShape2d& rCustomShape2d, + const std::vector* pCoordGuides, + const bool bReplaceGeoWidth, const bool bReplaceGeoHeight) +{ + if (pCoordGuides && !bReplaceGeoWidth && !bReplaceGeoHeight + && rParam.Type == css::drawing::EnhancedCustomShapeParameterType::EQUATION) + { + sal_Int32 nIndex = -1; + rParam.Value >>= nIndex; + if (nIndex >= 0 && o3tl::make_unsigned(nIndex) < pCoordGuides->size() + && !(*pCoordGuides)[nIndex].isEmpty()) + return (*pCoordGuides)[nIndex]; + } + return OString::number( + GetCustomGeometryPointValue(rParam, rCustomShape2d, bReplaceGeoWidth, bReplaceGeoHeight)); +} + void prepareGluePoints(std::vector& rGuideList, const css::uno::Sequence& aEquations, const uno::Sequence& rGluePoints, @@ -5305,9 +5718,50 @@ bool DrawingML::WriteCustomGeometry( std::vector aGuideList; // for now only for prepareTextArea(aCustomShape2d, aGuideList, aTextAreaRect); prepareGluePoints(aGuideList, aEquationSeq, aGluePoints, nViewBoxWidth, nViewBoxHeight); + + // Convert the shape's "Equations" into guides ("f0", "f1", ...) from the + // parsed expression tree (keeps width/height symbolic and handles the + // trigonometric functions). These are kept apart from aGuideList because they + // reference each other by name, which IsValidOOXMLFormula() (used to validate + // the text-area/glue-point guides below) does not accept. + std::vector aEquationGuides; + std::set aUsedAdjustments; + std::vector aEqNames; + const bool bHaveEquationGuides = lcl_CreateOoxmlGuides( + aCustomShape2d, aEquationSeq, aEquationGuides, aUsedAdjustments, aEqNames); + // Declare the referenced adjustment values in holding their current + // value, so the equation guides stay driven by the shape's handles. + std::vector aAdjGuides; + if (bHaveEquationGuides) + { + for (const sal_Int32 nAdjustment : aUsedAdjustments) + { + css::drawing::EnhancedCustomShapeParameter aParam; + aParam.Type = css::drawing::EnhancedCustomShapeParameterType::ADJUSTMENT; + aParam.Value <<= nAdjustment; + double fValue = 0.0; + aCustomShape2d.GetParameter(fValue, aParam, false, false); + aAdjGuides.push_back({ "adj" + OString::number(nAdjustment + 1), + "val " + OString::number(std::lround(fValue)) }); + } + } + // Path coordinates and handles reference an equation by the token (guide name, + // literal, adjustment or w/h) holding its value. + const std::vector* pEqNames = bHaveEquationGuides ? &aEqNames : nullptr; + mpFS->startElementNS(XML_a, XML_custGeom); - mpFS->singleElementNS(XML_a, XML_avLst); - if (aGuideList.empty()) + if (aAdjGuides.empty()) + { + mpFS->singleElementNS(XML_a, XML_avLst); + } + else + { + mpFS->startElementNS(XML_a, XML_avLst); + for (auto const& elem : aAdjGuides) + mpFS->singleElementNS(XML_a, XML_gd, XML_name, elem.sName, XML_fmla, elem.sFormula); + mpFS->endElementNS(XML_a, XML_avLst); + } + if (aGuideList.empty() && aEquationGuides.empty()) { mpFS->singleElementNS(XML_a, XML_gdLst); } @@ -5319,9 +5773,26 @@ bool DrawingML::WriteCustomGeometry( assert(IsValidOOXMLFormula(OUString::fromUtf8(elem.sFormula))); mpFS->singleElementNS(XML_a, XML_gd, XML_name, elem.sName, XML_fmla, elem.sFormula); } + for (auto const& elem : aEquationGuides) + mpFS->singleElementNS(XML_a, XML_gd, XML_name, elem.sName, XML_fmla, elem.sFormula); mpFS->endElementNS(XML_a, XML_gdLst); } - mpFS->singleElementNS(XML_a, XML_ahLst); + + // Adjust handles. They reference the avLst/gdLst guides written above, so we + // can only write them when the equation guides were created successfully. + uno::Sequence aHandles; + if (bHaveEquationGuides) + { + auto pHandlesProp = std::find_if( + std::cbegin(*pGeometrySeq), std::cend(*pGeometrySeq), + [](const beans::PropertyValue& rProp) { return rProp.Name == "Handles"; }); + if (pHandlesProp != std::cend(*pGeometrySeq)) + pHandlesProp->Value >>= aHandles; + } + if (aHandles.hasElements()) + lcl_WriteAdjustHandles(mpFS, aHandles, pEqNames, aUsedAdjustments); + else + mpFS->singleElementNS(XML_a, XML_ahLst); if (!aGuideList.empty()) { @@ -5419,7 +5890,7 @@ bool DrawingML::WriteCustomGeometry( { bOK = WriteCustomGeometrySegment(rSegment.Command, k, aPairs, nPairIndex, fCurrentX, fCurrentY, bCurrentValid, aCustomShape2d, - bReplaceGeoWidth, bReplaceGeoHeight); + bReplaceGeoWidth, bReplaceGeoHeight, pEqNames); } } // end loop over all commands of subpath // finish this subpath in any case @@ -5444,7 +5915,7 @@ bool DrawingML::WriteCustomGeometrySegment( const uno::Sequence& rPairs, sal_Int32& rnPairIndex, double& rfCurrentX, double& rfCurrentY, bool& rbCurrentValid, const EnhancedCustomShape2d& rCustomShape2d, const bool bReplaceGeoWidth, - const bool bReplaceGeoHeight) + const bool bReplaceGeoHeight, const std::vector* pCoordGuides) { switch (eCommand) { @@ -5455,7 +5926,7 @@ bool DrawingML::WriteCustomGeometrySegment( mpFS->startElementNS(XML_a, XML_moveTo); WriteCustomGeometryPoint(rPairs[rnPairIndex], rCustomShape2d, bReplaceGeoWidth, - bReplaceGeoHeight); + bReplaceGeoHeight, pCoordGuides); mpFS->endElementNS(XML_a, XML_moveTo); rCustomShape2d.GetParameter(rfCurrentX, rPairs[rnPairIndex].First, bReplaceGeoWidth, false); @@ -5476,14 +5947,14 @@ bool DrawingML::WriteCustomGeometrySegment( { mpFS->startElementNS(XML_a, XML_lnTo); WriteCustomGeometryPoint(rPairs[rnPairIndex], rCustomShape2d, bReplaceGeoWidth, - bReplaceGeoHeight); + bReplaceGeoHeight, pCoordGuides); mpFS->endElementNS(XML_a, XML_lnTo); } else { mpFS->startElementNS(XML_a, XML_moveTo); WriteCustomGeometryPoint(rPairs[rnPairIndex], rCustomShape2d, bReplaceGeoWidth, - bReplaceGeoHeight); + bReplaceGeoHeight, pCoordGuides); mpFS->endElementNS(XML_a, XML_moveTo); } rCustomShape2d.GetParameter(rfCurrentX, rPairs[rnPairIndex].First, bReplaceGeoWidth, @@ -5503,7 +5974,7 @@ bool DrawingML::WriteCustomGeometrySegment( for (sal_uInt8 i = 0; i <= 2; ++i) { WriteCustomGeometryPoint(rPairs[rnPairIndex + i], rCustomShape2d, bReplaceGeoWidth, - bReplaceGeoHeight); + bReplaceGeoHeight, pCoordGuides); } mpFS->endElementNS(XML_a, XML_cubicBezTo); rCustomShape2d.GetParameter(rfCurrentX, rPairs[rnPairIndex + 2].First, bReplaceGeoWidth, @@ -5709,7 +6180,7 @@ bool DrawingML::WriteCustomGeometrySegment( // faulty path, but we continue with the target point mpFS->startElementNS(XML_a, XML_moveTo); WriteCustomGeometryPoint(rPairs[rnPairIndex], rCustomShape2d, bReplaceGeoWidth, - bReplaceGeoHeight); + bReplaceGeoHeight, pCoordGuides); mpFS->endElementNS(XML_a, XML_moveTo); } rfCurrentX = fX; @@ -5727,7 +6198,7 @@ bool DrawingML::WriteCustomGeometrySegment( for (sal_uInt8 i = 0; i < 2; ++i) { WriteCustomGeometryPoint(rPairs[rnPairIndex + i], rCustomShape2d, bReplaceGeoWidth, - bReplaceGeoHeight); + bReplaceGeoHeight, pCoordGuides); } mpFS->endElementNS(XML_a, XML_quadBezTo); rCustomShape2d.GetParameter(rfCurrentX, rPairs[rnPairIndex + 1].First, bReplaceGeoWidth, @@ -5777,14 +6248,14 @@ bool DrawingML::WriteCustomGeometrySegment( void DrawingML::WriteCustomGeometryPoint( const drawing::EnhancedCustomShapeParameterPair& rParamPair, const EnhancedCustomShape2d& rCustomShape2d, const bool bReplaceGeoWidth, - const bool bReplaceGeoHeight) + const bool bReplaceGeoHeight, const std::vector* pCoordGuides) { - sal_Int32 nX - = GetCustomGeometryPointValue(rParamPair.First, rCustomShape2d, bReplaceGeoWidth, false); - sal_Int32 nY - = GetCustomGeometryPointValue(rParamPair.Second, rCustomShape2d, false, bReplaceGeoHeight); + OString sX = lcl_GetCustomGeometryCoordinate(rParamPair.First, rCustomShape2d, pCoordGuides, + bReplaceGeoWidth, false); + OString sY = lcl_GetCustomGeometryCoordinate(rParamPair.Second, rCustomShape2d, pCoordGuides, + false, bReplaceGeoHeight); - mpFS->singleElementNS(XML_a, XML_pt, XML_x, OString::number(nX), XML_y, OString::number(nY)); + mpFS->singleElementNS(XML_a, XML_pt, XML_x, sX, XML_y, sY); } void DrawingML::WriteEmptyCustomGeometry() diff --git a/oox/source/export/shapes.cxx b/oox/source/export/shapes.cxx index 272eed581faf..2ddf2e3f3e20 100644 --- a/oox/source/export/shapes.cxx +++ b/oox/source/export/shapes.cxx @@ -1103,8 +1103,10 @@ ShapeExport& ShapeExport::WriteCustomShape( const Reference< XShape >& xShape ) pFS->startElementNS(mnXmlNamespace, XML_spPr); // we export non-primitive shapes to custom geometry - // we also export non-ooxml shapes which have handles/equations to custom geometry, because - // we cannot convert ODF equations to DrawingML equations. TODO: see what binary DOC export filter does. + // we also export non-ooxml shapes which have handles/equations to custom geometry; their + // ODF equations, adjustment values and handles are converted to DrawingML guides in + // WriteCustomGeometry (a few constructs that cannot be expressed, e.g. polar handles, fall + // back to resolved coordinates) // but our WritePolyPolygon()/WriteCustomGeometry() functions are incomplete, therefore we use a denylist // we use a allowlist for shapes where mapping to MSO preset shape is not optimal bool bCustGeom = true; diff --git a/sd/qa/unit/export-tests-ooxml3.cxx b/sd/qa/unit/export-tests-ooxml3.cxx index dbfbc3747961..964b14beb5af 100644 --- a/sd/qa/unit/export-tests-ooxml3.cxx +++ b/sd/qa/unit/export-tests-ooxml3.cxx @@ -84,6 +84,24 @@ CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest3, testTdf169705_invalidValuesAOff) OUString::number(std::numeric_limits::max())); } +CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest3, testEquationGuidesExport) +{ + // The shape's "Equations" are converted into guides ("f0", "f1", ...) + // by reusing the same flattening the binary export filter uses. + createSdImpressDoc("pptx/tdf165670.pptx"); + save(TestFilter::PPTX); + + xmlDocUniquePtr pXmlDoc1 = parseExport(u"ppt/slides/slide1.xml"_ustr); + OString aGdLst = "/p:sld/p:cSld/p:spTree/p:sp/p:spPr/a:custGeom/a:gdLst"_ostr; + + // The equation guides follow the text-area (4) and glue-point (10) guides. + // Width/height stay symbolic ("w"/"h") instead of being baked to a fixed value. + assertXPath(pXmlDoc1, aGdLst + "/a:gd[@name='f0']", "fmla", u"*/ 690465 w 1"); + assertXPath(pXmlDoc1, aGdLst + "/a:gd[@name='f2']", "fmla", u"*/ 802433 h 1"); + // A guide referencing the result of an earlier guide by name. + assertXPath(pXmlDoc1, aGdLst + "/a:gd[@name='f1']", "fmla", u"*/ f0 1 2407298"); +} + CPPUNIT_TEST_FIXTURE(SdOOXMLExportTest3, testTdf165262) { createSdImpressDoc("ppt/tdf165262.ppt"); diff --git a/svx/source/customshapes/EnhancedCustomShapeFunctionParser.cxx b/svx/source/customshapes/EnhancedCustomShapeFunctionParser.cxx index b7c45af71a86..ba43ff262a9a 100644 --- a/svx/source/customshapes/EnhancedCustomShapeFunctionParser.cxx +++ b/svx/source/customshapes/EnhancedCustomShapeFunctionParser.cxx @@ -171,6 +171,7 @@ public: { return ExpressionFunct::EnumAdjustment; } + virtual sal_Int32 getIndex() const override { return mnIndex; } virtual EnhancedCustomShapeParameter fillNode( std::vector< EnhancedCustomShapeEquation >& /*rEquations*/, ExpressionNode* /*pOptionalArg*/, sal_uInt32 /*nFlags*/ ) override { EnhancedCustomShapeParameter aRet; @@ -211,6 +212,7 @@ public: { return ExpressionFunct::EnumEquation; } + virtual sal_Int32 getIndex() const override { return mnIndex; } virtual EnhancedCustomShapeParameter fillNode( std::vector< EnhancedCustomShapeEquation >& /*rEquations*/, ExpressionNode* /*pOptionalArg*/, sal_uInt32 /*nFlags*/ ) override { EnhancedCustomShapeParameter aRet; @@ -326,6 +328,10 @@ public: { return meFunct; } + virtual const ExpressionNode* getChild(sal_Int32 nIndex) const override + { + return nIndex == 0 ? mpArg.get() : nullptr; + } virtual EnhancedCustomShapeParameter fillNode( std::vector< EnhancedCustomShapeEquation >& rEquations, ExpressionNode* pOptionalArg, sal_uInt32 nFlags ) override { EnhancedCustomShapeParameter aRet; @@ -497,6 +503,14 @@ public: { return meFunct; } + virtual const ExpressionNode* getChild(sal_Int32 nIndex) const override + { + if (nIndex == 0) + return mpFirstArg.get(); + if (nIndex == 1) + return mpSecondArg.get(); + return nullptr; + } virtual EnhancedCustomShapeParameter fillNode( std::vector< EnhancedCustomShapeEquation >& rEquations, ExpressionNode* /*pOptionalArg*/, sal_uInt32 nFlags ) override { EnhancedCustomShapeParameter aRet; @@ -707,6 +721,16 @@ public: { return ExpressionFunct::TernaryIf; } + virtual const ExpressionNode* getChild(sal_Int32 nIndex) const override + { + switch (nIndex) + { + case 0: return mpFirstArg.get(); + case 1: return mpSecondArg.get(); + case 2: return mpThirdArg.get(); + default: return nullptr; + } + } virtual EnhancedCustomShapeParameter fillNode( std::vector< EnhancedCustomShapeEquation >& rEquations, ExpressionNode* /*pOptionalArg*/, sal_uInt32 nFlags ) override { EnhancedCustomShapeParameter aRet; diff --git a/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx b/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx index 467a0b999f08..e64aae82ed00 100644 --- a/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx +++ b/sw/qa/extras/ooxmlexport/ooxmlexport7.cxx @@ -1219,19 +1219,16 @@ CPPUNIT_TEST_FIXTURE(Test, testFlipAndRotateCustomShape) // point values depend on path size, values as of March 2022 assertXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path", "w", u"21600"); assertXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path", "h", u"21600"); - // check the first few coordinates of the polygon - CPPUNIT_ASSERT_DOUBLES_EQUAL( - 0, getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:moveTo/a:pt", "x").toInt32(), 1); - CPPUNIT_ASSERT_DOUBLES_EQUAL( - 15831, getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:moveTo/a:pt", "y").toInt32(), 1); - CPPUNIT_ASSERT_DOUBLES_EQUAL( - 6098, getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:lnTo[1]/a:pt", "x").toInt32(), 1); - CPPUNIT_ASSERT_DOUBLES_EQUAL( - 10062, getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:lnTo[1]/a:pt", "y").toInt32(), 1); - CPPUNIT_ASSERT_DOUBLES_EQUAL( - 13284, getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:lnTo[4]/a:pt", "x").toInt32(), 1); - CPPUNIT_ASSERT_DOUBLES_EQUAL( - 6098, getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:lnTo[4]/a:pt", "y").toInt32(), 1); + // This shape (mso-spt89) has adjustment handles, so it is exported as an + // adjustable custom geometry: its adjustment values are declared in , + // the handles in , and the adjustment-dependent path coordinates + // reference the guides (e.g. "f5") instead of being baked to fixed + // numbers. A literal coordinate (here the moveTo x) stays plain. + assertXPath(pXmlDoc, "//a:custGeom/a:avLst/a:gd", 3); + assertXPath(pXmlDoc, "//a:custGeom/a:ahLst/a:ahXY", 2); + assertXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:moveTo/a:pt", "x", u"0"); + CPPUNIT_ASSERT( + getXPath(pXmlDoc, "//a:custGeom/a:pathLst/a:path/a:moveTo/a:pt", "y").startsWith("f")); // check path is closed assertXPath(pXmlDoc, "//a:close", 1); }