/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
* This file is part of the LibreOffice project.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#include "WpsContext.hxx"
#include "WpgContext.hxx"
#include "WordprocessingCanvasContext.hxx"
#include <basegfx/matrix/b2dhommatrix.hxx>
#include <basegfx/tuple/b2dtuple.hxx>
#include <comphelper/propertyvalue.hxx>
#include <comphelper/sequence.hxx>
#include <comphelper/sequenceashashmap.hxx>
#include <drawingml/customshapegeometry.hxx>
#include <drawingml/customshapeproperties.hxx>
#include <drawingml/fontworkhelpers.hxx>
#include <drawingml/textbody.hxx>
#include <drawingml/textbodyproperties.hxx>
#include <oox/drawingml/color.hxx>
#include <oox/drawingml/connectorshapecontext.hxx>
#include <oox/drawingml/drawingmltypes.hxx>
#include <oox/drawingml/shape.hxx>
#include <oox/drawingml/shapepropertymap.hxx>
#include <oox/helper/attributelist.hxx>
#include <oox/token/namespaces.hxx>
#include <oox/token/tokens.hxx>
#include <svx/svdoashp.hxx>
#include <com/sun/star/beans/PropertyAttribute.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
#include <com/sun/star/beans/XPropertySetInfo.hpp>
#include <com/sun/star/beans/XPropertyState.hpp>
#include <com/sun/star/container/XEnumerationAccess.hpp>
#include <com/sun/star/drawing/HomogenMatrix3.hpp>
#include <com/sun/star/drawing/TextHorizontalAdjust.hpp>
#include <com/sun/star/geometry/IntegerRectangle2D.hpp>
#include <com/sun/star/lang/XServiceInfo.hpp>
#include <com/sun/star/text/XText.hpp>
#include <com/sun/star/text/XTextCursor.hpp>
#include <com/sun/star/text/WritingMode.hpp>
#include <com/sun/star/text/WritingMode2.hpp>
#include <optional>
using namespace com::sun::star;
namespace
{
bool lcl_getTextPropsFromFrameText(const uno::Reference<text::XText>& xText,
std::vector<beans::PropertyValue>& rTextPropVec)
{
if (!xText.is())
return false;
uno::Reference<text::XTextCursor> xTextCursor = xText->createTextCursor();
xTextCursor->gotoStart(false);
xTextCursor->gotoEnd(true);
uno::Reference<container::XEnumerationAccess> paraEnumAccess(xText, uno::UNO_QUERY);
if (!paraEnumAccess.is())
return false;
uno::Reference<container::XEnumeration> paraEnum(paraEnumAccess->createEnumeration());
while (paraEnum->hasMoreElements())
{
uno::Reference<text::XTextRange> xParagraph(paraEnum->nextElement(), uno::UNO_QUERY);
uno::Reference<container::XEnumerationAccess> runEnumAccess(xParagraph, uno::UNO_QUERY);
if (!runEnumAccess.is())
continue;
uno::Reference<container::XEnumeration> runEnum = runEnumAccess->createEnumeration();
while (runEnum->hasMoreElements())
{
uno::Reference<text::XTextRange> xRun(runEnum->nextElement(), uno::UNO_QUERY);
if (xRun->getString().isEmpty())
continue;
uno::Reference<beans::XPropertySet> xRunPropSet(xRun, uno::UNO_QUERY);
if (!xRunPropSet.is())
continue;
auto xRunPropSetInfo = xRunPropSet->getPropertySetInfo();
if (!xRunPropSetInfo.is())
continue;
// We have found a non-empty run. Collect its properties.
auto aRunPropInfoSequence = xRunPropSetInfo->getProperties();
for (const beans::Property& aProp : aRunPropInfoSequence)
{
rTextPropVec.push_back(comphelper::makePropertyValue(
aProp.Name, xRunPropSet->getPropertyValue(aProp.Name)));
}
return true;
}
}
return false;
}
// CharInteropGrabBag puts all attributes of an element into a property with Name="attributes" and
// Value being a sequence of the attributes. This methods finds the value of an individual rName
// attribute and puts it into rValue parameter. If it does not find it, rValue is unchanged and
// the method returns false, otherwise it returns true.
bool lcl_getAttributeAsString(const uno::Sequence<beans::PropertyValue>& aPropertyValueAsSeq,
const OUString& rName, OUString& rValue)
{
comphelper::SequenceAsHashMap aPropertyValueAsMap(aPropertyValueAsSeq);
uno::Sequence<beans::PropertyValue> aAttributesSeq;
if (!((aPropertyValueAsMap.getValue(u"attributes"_ustr) >>= aAttributesSeq)
&& aAttributesSeq.hasElements()))
return false;
comphelper::SequenceAsHashMap aAttributesMap(aAttributesSeq);
OUString sRet;
if (!(aAttributesMap.getValue(rName) >>= sRet))
return false;
rValue = sRet;
return true;
}
// Same as above for a number as attribute value
bool lcl_getAttributeAsNumber(const uno::Sequence<beans::PropertyValue>& rPropertyValueAsSeq,
const OUString& rName, sal_Int32& rValue)
{
comphelper::SequenceAsHashMap aPropertyValueAsMap(rPropertyValueAsSeq);
uno::Sequence<beans::PropertyValue> aAttributesSeq;
if (!((aPropertyValueAsMap.getValue(u"attributes"_ustr) >>= aAttributesSeq)
&& aAttributesSeq.hasElements()))
return false;
comphelper::SequenceAsHashMap aAttributesMap(aAttributesSeq);
sal_Int32 nRet;
if (!(aAttributesMap.getValue(rName) >>= nRet))
return false;
rValue = nRet;
return true;
}
void lcl_getColorTransformationsFromPropSeq(const uno::Sequence<beans::PropertyValue>& rPropSeq,
oox::drawingml::Color& rColor)
{
auto isValidPropName = [](const OUString& rName) -> bool {
return rName == u"tint" || rName == u"shade" || rName == u"alpha" || rName == u"hueMod"
|| rName == u"sat" || rName == u"satOff" || rName == u"satMod" || rName == u"lum"
|| rName == u"lumOff" || rName == u"lumMod";
};
for (auto it = rPropSeq.begin(); it < rPropSeq.end(); ++it)
{
if (isValidPropName((*it).Name))
{
uno::Sequence<beans::PropertyValue> aValueSeq;
sal_Int32 nNumber(0); // dummy value to make compiler happy, "val" should exist
if (((*it).Value >>= aValueSeq)
&& lcl_getAttributeAsNumber(aValueSeq, u"val"_ustr, nNumber))
{
// char w14:alpha contains transparency, whereas shape fill a:alpha contains opacity.
if ((*it).Name == u"alpha")
rColor.addTransformation(
oox::NMSP_dml | oox::AttributeConversion::decodeToken((*it).Name),
oox::drawingml::MAX_PERCENT - nNumber);
else
rColor.addTransformation(
oox::NMSP_w14 | oox::AttributeConversion::decodeToken((*it).Name), nNumber);
}
}
}
}
// Expected: rPropSeq contains a property "schemeClr" or a property "srgbClr".
bool lcl_getColorFromPropSeq(const uno::Sequence<beans::PropertyValue>& rPropSeq,
oox::drawingml::Color& rColor)
{
bool bColorFound = false;
comphelper::SequenceAsHashMap aPropMap(rPropSeq);
uno::Sequence<beans::PropertyValue> aColorDetailSeq;
if (aPropMap.getValue(u"schemeClr"_ustr) >>= aColorDetailSeq)
{
OUString sColorString;
bColorFound = lcl_getAttributeAsString(aColorDetailSeq, u"val"_ustr, sColorString);
if (bColorFound)
{
sal_Int32 nColorToken = oox::AttributeConversion::decodeToken(sColorString);
rColor.setSchemeClr(nColorToken);
rColor.setSchemeName(sColorString);
}
}
if (!bColorFound && (aPropMap.getValue(u"srgbClr"_ustr) >>= aColorDetailSeq))
{
OUString sColorString;
bColorFound = lcl_getAttributeAsString(aColorDetailSeq, u"val"_ustr, sColorString);
if (bColorFound)
{
sal_Int32 nColor = oox::AttributeConversion::decodeIntegerHex(sColorString);
rColor.setSrgbClr(nColor);
}
}
// Without color, color transformations are pointless.
if (bColorFound)
lcl_getColorTransformationsFromPropSeq(aColorDetailSeq, rColor);
return bColorFound;
}
void lcl_getFillDetailsFromPropSeq(const uno::Sequence<beans::PropertyValue>& rTextFillSeq,
oox::drawingml::FillProperties& rFillProperties)
{
// rTextFillSeq should have an item containing either "noFill" or "solidFill" or "gradFill"
// property.
if (!rTextFillSeq.hasElements())
return;
comphelper::SequenceAsHashMap aTextFillMap(rTextFillSeq);
if (aTextFillMap.contains(u"noFill"_ustr))
{
rFillProperties.moFillType = oox::XML_noFill;
return;
}
uno::Sequence<beans::PropertyValue> aPropSeq;
if ((aTextFillMap.getValue(u"solidFill"_ustr) >>= aPropSeq) && aPropSeq.hasElements())
{
rFillProperties.moFillType = oox::XML_solidFill;
lcl_getColorFromPropSeq(aPropSeq, rFillProperties.maFillColor);
return;
}
if ((aTextFillMap.getValue(u"gradFill"_ustr) >>= aPropSeq) && aPropSeq.hasElements())
{
rFillProperties.moFillType = oox::XML_gradFill;
// aPropSeq should have two items. One is "gsLst" for the stop colors, the other is
// either "lin" or "path" for the kind of gradient.
// First get stop colors
comphelper::SequenceAsHashMap aPropMap(aPropSeq);
uno::Sequence<beans::PropertyValue> aGsLstSeq;
if (aPropMap.getValue(u"gsLst"_ustr) >>= aGsLstSeq)
{
for (auto it = aGsLstSeq.begin(); it < aGsLstSeq.end(); ++it)
{
// (*it) is a bean::PropertyValue with Name="gs". Its Value is a property sequence.
uno::Sequence<beans::PropertyValue> aColorStopSeq;
if ((*it).Value >>= aColorStopSeq)
{
// aColorStopSeq should have an item for the color and an item for the position
sal_Int32 nPos;
oox::drawingml::Color aColor;
if (lcl_getAttributeAsNumber(aColorStopSeq, u"pos"_ustr, nPos)
&& lcl_getColorFromPropSeq(aColorStopSeq, aColor))
{
// The position in maGradientStops is relative, thus in range [0.0;1.0].
double fPos = nPos / 100000.0;
rFillProperties.maGradientProps.maGradientStops.insert({ fPos, aColor });
}
}
}
}
// Now determine kind of gradient.
uno::Sequence<beans::PropertyValue> aKindSeq;
if (aPropMap.getValue(u"lin"_ustr) >>= aKindSeq)
{
// aKindSeq contains the attributes "ang" and "scaled"
sal_Int32 nAngle; // in 1/60000 deg
if (lcl_getAttributeAsNumber(aKindSeq, u"ang"_ustr, nAngle))
rFillProperties.maGradientProps.moShadeAngle = nAngle;
OUString sScaledString;
if (lcl_getAttributeAsString(aKindSeq, u"scaled"_ustr, sScaledString))
rFillProperties.maGradientProps.moShadeScaled
= sScaledString == u"1" || sScaledString == u"true";
return;
}
if (aPropMap.getValue(u"path"_ustr) >>= aKindSeq)
{
// aKindSeq contains the attribute "path" for the kind of path and a property "fillToRect"
// which defines the center rectangle of the gradient. The property "a:tileRect" known from
// fill of shapes does not exist in w14 namespace.
OUString sKind;
if (lcl_getAttributeAsString(aKindSeq, u"path"_ustr, sKind))
rFillProperties.maGradientProps.moGradientPath
= oox::AttributeConversion::decodeToken(sKind);
comphelper::SequenceAsHashMap aKindMap(aKindSeq);
uno::Sequence<beans::PropertyValue> aFillToRectSeq;
if (aKindMap.getValue(u"fillToRect"_ustr) >>= aFillToRectSeq)
{
// The values l, t, r and b are not coordinates, but determine an offset from the
// edge of the bounding box of the shape. This unusual meaning of X1, Y1, X2 and
// Y2 is needed for method pushToPropMap() of FillProperties.
geometry::IntegerRectangle2D aRect;
if (!lcl_getAttributeAsNumber(aFillToRectSeq, u"l"_ustr, aRect.X1))
aRect.X1 = 0;
if (!lcl_getAttributeAsNumber(aFillToRectSeq, u"t"_ustr, aRect.Y1))
aRect.Y1 = 0;
if (!lcl_getAttributeAsNumber(aFillToRectSeq, u"r"_ustr, aRect.X2))
aRect.X2 = 0;
if (!lcl_getAttributeAsNumber(aFillToRectSeq, u"b"_ustr, aRect.Y2))
aRect.Y2 = 0;
rFillProperties.maGradientProps.moFillToRect = aRect;
}
}
return;
}
}
void lcl_getLineDetailsFromPropSeq(const uno::Sequence<beans::PropertyValue>& rTextOutlineSeq,
oox::drawingml::LineProperties& rLineProperties)
{
if (!rTextOutlineSeq.hasElements())
{
rLineProperties.maLineFill.moFillType = oox::XML_noFill; // MS Office default
return;
}
// aTextOulineSeq contains e.g. "attributes" {w, cap, cmpd, ctr}, either
// "solidFill" or "gradFill or "noFill", and "prstDash" and "lineJoint" properties.
// Fill
lcl_getFillDetailsFromPropSeq(rTextOutlineSeq, rLineProperties.maLineFill);
// LineJoint
comphelper::SequenceAsHashMap aTextOutlineMap(rTextOutlineSeq);
if (aTextOutlineMap.contains(u"bevel"_ustr))
rLineProperties.moLineJoint = oox::XML_bevel;
else if (aTextOutlineMap.contains(u"round"_ustr))
rLineProperties.moLineJoint = oox::XML_round;
else if (aTextOutlineMap.contains(u"miter"_ustr))
{
// LineProperties has no member to store a miter limit. Therefore some heuristic is
// added here. 0 is default for attribute "lim" in MS Office. It is rendered same as bevel.
sal_Int32 nMiterLimit
= aTextOutlineMap.getUnpackedValueOrDefault(u"lim"_ustr, sal_Int32(0));
if (nMiterLimit == 0)
rLineProperties.moLineJoint = oox::XML_bevel;
else
rLineProperties.moLineJoint = oox::XML_miter;
}
// Dash
uno::Sequence<beans::PropertyValue> aDashSeq;
if (aTextOutlineMap.getValue(u"prstDash"_ustr) >>= aDashSeq)
{
// aDashSeq contains the attribute "val" with the kind of dash, e.g. "sysDot"
OUString sDashKind;
if (lcl_getAttributeAsString(aDashSeq, u"val"_ustr, sDashKind))
rLineProperties.moPresetDash = oox::AttributeConversion::decodeToken(sDashKind);
}
OUString sCapKind;
if (lcl_getAttributeAsString(rTextOutlineSeq, u"cap"_ustr, sCapKind))
rLineProperties.moLineCap = oox::AttributeConversion::decodeToken(sCapKind);
// Width
sal_Int32 nWidth; // EMU
if (lcl_getAttributeAsNumber(rTextOutlineSeq, u"w"_ustr, nWidth))
rLineProperties.moLineWidth = nWidth;
// Compound. LineProperties has a member for it, however Fontwork can currently only render "sng".
OUString sCompoundKind;
if (lcl_getAttributeAsString(rTextOutlineSeq, u"cmpd"_ustr, sCompoundKind))
rLineProperties.moLineCompound = oox::AttributeConversion::decodeToken(sCompoundKind);
// Align. LineProperties has no member for attribute "algn".
return;
}
oox::drawingml::LineProperties
lcl_generateLinePropertiesFromTextProps(const comphelper::SequenceAsHashMap& aTextPropMap)
{
oox::drawingml::LineProperties aLineProperties;
aLineProperties.maLineFill.moFillType = oox::XML_noFill; // default
// Get property "textOutline" from aTextPropMap
uno::Sequence<beans::PropertyValue> aCharInteropGrabBagSeq;
if (!(aTextPropMap.getValue(u"CharInteropGrabBag"_ustr) >>= aCharInteropGrabBagSeq))
return aLineProperties;
if (!aCharInteropGrabBagSeq.hasElements())
return aLineProperties;
comphelper::SequenceAsHashMap aCharInteropGrabBagMap(aCharInteropGrabBagSeq);
beans::PropertyValue aProp;
if (!(aCharInteropGrabBagMap.getValue(u"CharTextOutlineTextEffect"_ustr) >>= aProp))
return aLineProperties;
uno::Sequence<beans::PropertyValue> aTextOutlineSeq;
if (!(aProp.Name == "textOutline" && (aProp.Value >>= aTextOutlineSeq)
&& aTextOutlineSeq.hasElements()))
return aLineProperties;
// Copy line properties from aTextOutlineSeq to aLineProperties
lcl_getLineDetailsFromPropSeq(aTextOutlineSeq, aLineProperties);
return aLineProperties;
}
oox::drawingml::FillProperties
lcl_generateFillPropertiesFromTextProps(const comphelper::SequenceAsHashMap& rTextPropMap)
{
oox::drawingml::FillProperties aFillProperties;
aFillProperties.moFillType = oox::XML_solidFill; // default
// Theme color supersedes direct color. textFill supersedes theme color. Theme color and textFill
// are in CharInteropGrabBag.
uno::Sequence<beans::PropertyValue> aCharInteropGrabBagSeq;
if ((rTextPropMap.getValue(u"CharInteropGrabBag"_ustr) >>= aCharInteropGrabBagSeq)
&& aCharInteropGrabBagSeq.hasElements())
{
// Handle case textFill
comphelper::SequenceAsHashMap aCharInteropGrabBagMap(aCharInteropGrabBagSeq);
beans::PropertyValue aProp;
if (aCharInteropGrabBagMap.getValue(u"CharTextFillTextEffect"_ustr) >>= aProp)
{
uno::Sequence<beans::PropertyValue> aTextFillSeq;
if (aProp.Name == "textFill" && (aProp.Value >>= aTextFillSeq)
&& aTextFillSeq.hasElements())
{
// Copy fill properties from aTextFillSeq to aFillProperties
lcl_getFillDetailsFromPropSeq(aTextFillSeq, aFillProperties);
return aFillProperties;
}
}
// no textFill, look for theme color, tint and shade
bool bColorFound(false);
OUString sColorString;
if (aCharInteropGrabBagMap.getValue(u"CharThemeOriginalColor"_ustr) >>= sColorString)
{
sal_Int32 nThemeOrigColor = oox::AttributeConversion::decodeIntegerHex(sColorString);
aFillProperties.maFillColor.setSrgbClr(nThemeOrigColor);
bColorFound = true;
}
if (aCharInteropGrabBagMap.getValue(u"CharThemeColor"_ustr) >>= sColorString)
{
sal_Int32 nColorToken = oox::AttributeConversion::decodeToken(sColorString);
aFillProperties.maFillColor.setSchemeClr(nColorToken);
aFillProperties.maFillColor.setSchemeName(sColorString);
bColorFound = true;
// A character color has shade or tint, a shape color has lumMod and lumOff.
OUString sTransformString;
if (aCharInteropGrabBagMap.getValue(u"CharThemeColorTint"_ustr) >>= sTransformString)
{
double fTint = oox::AttributeConversion::decodeIntegerHex(sTransformString);
fTint = fTint / 255.0 * oox::drawingml::MAX_PERCENT;
aFillProperties.maFillColor.addTransformation(OOX_TOKEN(w14, lumMod),
static_cast<sal_Int32>(fTint + 0.5));
double fOff = oox::drawingml::MAX_PERCENT - fTint;
aFillProperties.maFillColor.addTransformation(OOX_TOKEN(w14, lumOff),
static_cast<sal_Int32>(fOff + 0.5));
}
else if (aCharInteropGrabBagMap.getValue(u"CharThemeColorShade"_ustr)
>>= sTransformString)
{
double fShade = oox::AttributeConversion::decodeIntegerHex(sTransformString);
fShade = fShade / 255.0 * oox::drawingml::MAX_PERCENT;
aFillProperties.maFillColor.addTransformation(OOX_TOKEN(w14, lumMod),
static_cast<sal_Int32>(fShade + 0.5));
}
}
if (bColorFound)
return aFillProperties;
}
// Neither textFill nor theme color. Look for direct color.
sal_Int32 aCharColor = 0;
if (rTextPropMap.getValue(u"CharColor"_ustr) >>= aCharColor)
aFillProperties.maFillColor.setSrgbClr(aCharColor);
else
aFillProperties.maFillColor.setUnused();
return aFillProperties;
}
void lcl_applyShapePropsToShape(const uno::Reference<beans::XPropertySet>& xShapePropertySet,
const oox::drawingml::ShapePropertyMap& rShapeProps)
{
for (const auto& rProp : rShapeProps.makePropertyValueSequence())
{
xShapePropertySet->setPropertyValue(rProp.Name, rProp.Value);
}
}
void lcl_setTextAnchorFromTextProps(const uno::Reference<beans::XPropertySet>& xShapePropertySet,
const comphelper::SequenceAsHashMap& aTextPropMap)
{
// Fontwork does not evaluate paragraph alignment but uses text anchor instead
auto eHorzAdjust(drawing::TextHorizontalAdjust_CENTER);
sal_Int16 nParaAlign = sal_Int16(drawing::TextHorizontalAdjust_CENTER);
aTextPropMap.getValue(u"ParaAdjust"_ustr) >>= nParaAlign;
switch (nParaAlign)
{
case sal_Int16(style::ParagraphAdjust_LEFT):
eHorzAdjust = drawing::TextHorizontalAdjust_LEFT;
break;
case sal_Int16(style::ParagraphAdjust_RIGHT):
eHorzAdjust = drawing::TextHorizontalAdjust_RIGHT;
break;
default:
eHorzAdjust = drawing::TextHorizontalAdjust_CENTER;
}
xShapePropertySet->setPropertyValue(u"TextHorizontalAdjust"_ustr, uno::Any(eHorzAdjust));
xShapePropertySet->setPropertyValue(u"TextVerticalAdjust"_ustr,
uno::Any(drawing::TextVerticalAdjust_TOP));
}
void lcl_setTextPropsToShape(const uno::Reference<beans::XPropertySet>& xShapePropertySet,
std::vector<beans::PropertyValue>& aTextPropVec)
{
auto xShapePropertySetInfo = xShapePropertySet->getPropertySetInfo();
if (!xShapePropertySetInfo.is())
return;
for (size_t i = 0; i < aTextPropVec.size(); ++i)
{
if (xShapePropertySetInfo->hasPropertyByName(aTextPropVec[i].Name)
&& !(xShapePropertySetInfo->getPropertyByName(aTextPropVec[i].Name).Attributes
& beans::PropertyAttribute::READONLY)
&& aTextPropVec[i].Name != u"CharInteropGrabBag")
{
xShapePropertySet->setPropertyValue(aTextPropVec[i].Name, aTextPropVec[i].Value);
}
}
}
void lcl_applyUsedTextPropsToAllTextRuns(uno::Reference<text::XText>& xText,
const std::vector<beans::PropertyValue>& aTextPropVec)
{
if (!xText.is())
return;
uno::Reference<text::XTextCursor> xTextCursor = xText->createTextCursor();
xTextCursor->gotoStart(false);
xTextCursor->gotoEnd(true);
uno::Reference<container::XEnumerationAccess> paraEnumAccess(xText, uno::UNO_QUERY);
if (!paraEnumAccess.is())
return;
uno::Reference<container::XEnumeration> paraEnum(paraEnumAccess->createEnumeration());
while (paraEnum->hasMoreElements())
{
uno::Reference<text::XTextRange> xParagraph(paraEnum->nextElement(), uno::UNO_QUERY);
uno::Reference<container::XEnumerationAccess> runEnumAccess(xParagraph, uno::UNO_QUERY);
if (!runEnumAccess.is())
continue;
uno::Reference<container::XEnumeration> runEnum = runEnumAccess->createEnumeration();
while (runEnum->hasMoreElements())
{
uno::Reference<text::XTextRange> xRun(runEnum->nextElement(), uno::UNO_QUERY);
if (xRun->getString().isEmpty())
continue;
uno::Reference<beans::XPropertySet> xRunPropSet(xRun, uno::UNO_QUERY);
if (!xRunPropSet.is())
continue;
auto xRunPropSetInfo = xRunPropSet->getPropertySetInfo();
if (!xRunPropSetInfo.is())
continue;
for (size_t i = 0; i < aTextPropVec.size(); ++i)
{
if (xRunPropSetInfo->hasPropertyByName(aTextPropVec[i].Name)
&& !(xRunPropSetInfo->getPropertyByName(aTextPropVec[i].Name).Attributes
& beans::PropertyAttribute::READONLY))
xRunPropSet->setPropertyValue(aTextPropVec[i].Name, aTextPropVec[i].Value);
}
}
}
}
} // anonymous namespace
namespace oox::shape
{
WpsContext::WpsContext(ContextHandler2Helper const& rParent, uno::Reference<drawing::XShape> xShape,
const drawingml::ShapePtr& pMasterShapePtr,
const drawingml::ShapePtr& pShapePtr)
: ShapeContext(rParent, pMasterShapePtr, pShapePtr)
, mxShape(std::move(xShape))
{
if (mpShapePtr)
mpShapePtr->setWps(true);
if (const auto pParent = dynamic_cast<const WpgContext*>(&rParent))
m_bHasWPGParent = pParent->isFullWPGSupport();
else if (dynamic_cast<const WordprocessingCanvasContext*>(&rParent))
m_bHasWPGParent = true;
else
m_bHasWPGParent = false;
if ((pMasterShapePtr && pMasterShapePtr->isInWordprocessingCanvas())
|| dynamic_cast<const WordprocessingCanvasContext*>(&rParent) != nullptr)
pShapePtr->setWordprocessingCanvas(true);
}
WpsContext::~WpsContext() = default;
oox::core::ContextHandlerRef WpsContext::onCreateContext(sal_Int32 nElementToken,
const oox::AttributeList& rAttribs)
{
switch (getBaseToken(nElementToken))
{
case XML_wsp:
break;
case XML_cNvCnPr:
{
// It might be a connector shape in a wordprocessing canvas
// Replace the custom shape with a connector shape.
if (!mpShapePtr || !mpShapePtr->isInWordprocessingCanvas() || !mpMasterShapePtr)
break;
// Generate new shape
oox::drawingml::ShapePtr pShape = std::make_shared<oox::drawingml::Shape>(
u"com.sun.star.drawing.ConnectorShape"_ustr, false);
pShape->setConnectorShape(true);
pShape->setWps(true);
pShape->setWordprocessingCanvas(true);
// ToDo: Can only copy infos from mpShapePtr to pShape for which getter available.
pShape->setName(mpShapePtr->getName());
pShape->setId(mpShapePtr->getId());
pShape->setWPGChild(mpShapePtr->isWPGChild());
// And actually replace the shape.
mpShapePtr = pShape;
mpMasterShapePtr->getChildren().pop_back();
mpMasterShapePtr->getChildren().push_back(pShape);
return new oox::drawingml::ConnectorShapePropertiesContext(
*this, mpShapePtr, mpShapePtr->getConnectorShapeProperties());
}
case XML_bodyPr:
if (mxShape.is())
{
// no evaluation of attribute XML_rot, because Word ignores it, as of 2022-07.
uno::Reference<lang::XServiceInfo> xServiceInfo(mxShape, uno::UNO_QUERY);
uno::Reference<beans::XPropertySet> xPropertySet(mxShape, uno::UNO_QUERY);
sal_Int32 nVert = rAttribs.getToken(XML_vert, XML_horz);
if (nVert == XML_eaVert)
{
xPropertySet->setPropertyValue(u"TextWritingMode"_ustr,
uno::Any(text::WritingMode_TB_RL));
xPropertySet->setPropertyValue(u"WritingMode"_ustr,
uno::Any(text::WritingMode2::TB_RL));
}
else if (nVert == XML_mongolianVert)
{
xPropertySet->setPropertyValue(u"WritingMode"_ustr,
uno::Any(text::WritingMode2::TB_LR));
}
else if (nVert == XML_wordArtVert || nVert == XML_wordArtVertRtl)
{
// Multiline wordArtVert is not implemented yet.
// It will render all the text in 1 line.
// Map 'wordArtVertRtl' to 'wordArtVert', as they are the same now.
xPropertySet->setPropertyValue(u"WritingMode"_ustr,
uno::Any(text::WritingMode2::STACKED));
}
else if (nVert != XML_horz) // cases XML_vert and XML_vert270
{
// Hack to get same rendering as after the fix for tdf#87924. If shape rotation
// plus text direction results in upright text, use horizontal text direction.
// Remove hack when frame is able to rotate.
// Need transformation matrix since RotateAngle does not contain flip.
drawing::HomogenMatrix3 aMatrix;
xPropertySet->getPropertyValue(u"Transformation"_ustr) >>= aMatrix;
basegfx::B2DHomMatrix aTransformation;
aTransformation.set(0, 0, aMatrix.Line1.Column1);
aTransformation.set(0, 1, aMatrix.Line1.Column2);
aTransformation.set(0, 2, aMatrix.Line1.Column3);
aTransformation.set(1, 0, aMatrix.Line2.Column1);
aTransformation.set(1, 1, aMatrix.Line2.Column2);
aTransformation.set(1, 2, aMatrix.Line2.Column3);
// For this to be a valid 2D transform matrix, the last row must be [0,0,1]
assert(aMatrix.Line3.Column1 == 0);
assert(aMatrix.Line3.Column2 == 0);
assert(aMatrix.Line3.Column3 == 1);
basegfx::B2DTuple aScale;
basegfx::B2DTuple aTranslate;
double fRotate = 0;
double fShearX = 0;
aTransformation.decompose(aScale, aTranslate, fRotate, fShearX);
auto nRotate(static_cast<sal_uInt16>(NormAngle360(basegfx::rad2deg(fRotate))));
if ((nVert == XML_vert && nRotate == 270)
|| (nVert == XML_vert270 && nRotate == 90))
{
xPropertySet->setPropertyValue(u"WritingMode"_ustr,
uno::Any(text::WritingMode2::LR_TB));
// ToDo: Remember original vert value and remove hack on export.
}
else if (nVert == XML_vert)
xPropertySet->setPropertyValue(u"WritingMode"_ustr,
uno::Any(text::WritingMode2::TB_RL90));
else // nVert == XML_vert270
xPropertySet->setPropertyValue(u"WritingMode"_ustr,
uno::Any(text::WritingMode2::BT_LR));
}
if (bool bUpright = rAttribs.getBool(XML_upright, false))
{
uno::Sequence<beans::PropertyValue> aGrabBag;
xPropertySet->getPropertyValue(u"InteropGrabBag"_ustr) >>= aGrabBag;
sal_Int32 length = aGrabBag.getLength();
aGrabBag.realloc(length + 1);
auto pGrabBag = aGrabBag.getArray();
pGrabBag[length].Name = "Upright";
pGrabBag[length].Value <<= bUpright;
xPropertySet->setPropertyValue(u"InteropGrabBag"_ustr, uno::Any(aGrabBag));
}
if (xServiceInfo.is())
{
// Handle inset attributes for Writer textframes.
sal_Int32 aInsets[] = { XML_lIns, XML_tIns, XML_rIns, XML_bIns };
std::optional<sal_Int32> oInsets[4];
for (std::size_t i = 0; i < SAL_N_ELEMENTS(aInsets); ++i)
{
std::optional<OUString> oValue = rAttribs.getString(aInsets[i]);
if (oValue.has_value())
oInsets[i] = oox::drawingml::GetCoordinate(oValue.value());
else
// Defaults from the spec: left/right: 91440 EMU, top/bottom: 45720 EMU
oInsets[i]
= (aInsets[i] == XML_lIns || aInsets[i] == XML_rIns) ? 254 : 127;
}
const OUString aShapeProps[]
= { u"TextLeftDistance"_ustr, u"TextUpperDistance"_ustr,
u"TextRightDistance"_ustr, u"TextLowerDistance"_ustr };
for (std::size_t i = 0; i < SAL_N_ELEMENTS(aShapeProps); ++i)
if (oInsets[i])
xPropertySet->setPropertyValue(aShapeProps[i], uno::Any(*oInsets[i]));
}
// Handle text vertical adjustment inside a text frame
if (rAttribs.hasAttribute(XML_anchor))
{
drawing::TextVerticalAdjust eAdjust
= drawingml::GetTextVerticalAdjust(rAttribs.getToken(XML_anchor, XML_t));
xPropertySet->setPropertyValue(u"TextVerticalAdjust"_ustr, uno::Any(eAdjust));
}
// Apply character color of the shape to the shape's textbox.
uno::Reference<text::XText> xText(mxShape, uno::UNO_QUERY);
uno::Any xCharColor = xPropertySet->getPropertyValue(u"CharColor"_ustr);
Color aColor = COL_AUTO;
if ((xCharColor >>= aColor) && aColor != COL_AUTO)
{
// tdf#135923 Apply character color of the shape to the textrun
// when the character color of the textrun is default.
// tdf#153791 But only if the run has no background color (shd element in OOXML)
if (uno::Reference<container::XEnumerationAccess> paraEnumAccess{
xText, uno::UNO_QUERY })
{
uno::Reference<container::XEnumeration> paraEnum(
paraEnumAccess->createEnumeration());
while (paraEnum->hasMoreElements())
{
uno::Reference<text::XTextRange> xParagraph(paraEnum->nextElement(),
uno::UNO_QUERY);
uno::Reference<container::XEnumerationAccess> runEnumAccess(
xParagraph, uno::UNO_QUERY);
if (!runEnumAccess.is())
continue;
if (uno::Reference<beans::XPropertySet> xParaPropSet{ xParagraph,
uno::UNO_QUERY })
if ((xParaPropSet->getPropertyValue(u"ParaBackColor"_ustr)
>>= aColor)
&& aColor != COL_AUTO)
continue;
uno::Reference<container::XEnumeration> runEnum
= runEnumAccess->createEnumeration();
while (runEnum->hasMoreElements())
{
uno::Reference<text::XTextRange> xRun(runEnum->nextElement(),
uno::UNO_QUERY);
const uno::Reference<beans::XPropertyState> xRunState(
xRun, uno::UNO_QUERY);
if (!xRunState
|| xRunState->getPropertyState(u"CharColor"_ustr)
== beans::PropertyState_DEFAULT_VALUE)
{
uno::Reference<beans::XPropertySet> xRunPropSet(xRun,
uno::UNO_QUERY);
if (!xRunPropSet)
continue;
if ((xRunPropSet->getPropertyValue(u"CharBackColor"_ustr)
>>= aColor)
&& aColor != COL_AUTO)
continue;
if (!(xRunPropSet->getPropertyValue(u"CharColor"_ustr)
>>= aColor)
|| aColor == COL_AUTO)
xRunPropSet->setPropertyValue(u"CharColor"_ustr,
xCharColor);
}
}
}
}
}
auto nWrappingType = rAttribs.getToken(XML_wrap, XML_square);
xPropertySet->setPropertyValue(u"TextWordWrap"_ustr,
uno::Any(nWrappingType == XML_square));
return this;
}
else if (m_bHasWPGParent && mpShapePtr)
{
// this WPS context has to be inside a WPG shape, so the <BodyPr> element
// cannot be applied to mxShape member, use mpShape instead, and after the
// the parent shape finished, apply it for its children.
mpShapePtr->setWPGChild(true);
oox::drawingml::TextBodyPtr pTextBody;
pTextBody.reset(new oox::drawingml::TextBody());
if (rAttribs.hasAttribute(XML_anchor))
{
drawing::TextVerticalAdjust eAdjust
= drawingml::GetTextVerticalAdjust(rAttribs.getToken(XML_anchor, XML_t));
pTextBody->getTextProperties().meVA = eAdjust;
}
sal_Int32 aInsets[] = { XML_lIns, XML_tIns, XML_rIns, XML_bIns };
for (int i = 0; i < 4; ++i)
{
if (rAttribs.hasAttribute(XML_lIns))
{
std::optional<OUString> oValue = rAttribs.getString(aInsets[i]);
if (oValue.has_value())
pTextBody->getTextProperties().moInsets[i]
= oox::drawingml::GetCoordinate(oValue.value());
else
// Defaults from the spec: left/right: 91440 EMU, top/bottom: 45720 EMU
pTextBody->getTextProperties().moInsets[i]
= (aInsets[i] == XML_lIns || aInsets[i] == XML_rIns) ? 254 : 127;
}
}
mpShapePtr->setTextBody(pTextBody);
}
break;
case XML_noAutofit:
case XML_spAutoFit:
{
uno::Reference<lang::XServiceInfo> xServiceInfo(mxShape, uno::UNO_QUERY);
// We can't use oox::drawingml::TextBodyPropertiesContext here, as this
// is a child context of bodyPr, so the shape is already sent: we need
// to alter the XShape directly.
uno::Reference<beans::XPropertySet> xPropertySet(mxShape, uno::UNO_QUERY);
if (xPropertySet.is())
{
if (xServiceInfo->supportsService(u"com.sun.star.text.TextFrame"_ustr))
xPropertySet->setPropertyValue(
u"FrameIsAutomaticHeight"_ustr,
uno::Any(getBaseToken(nElementToken) == XML_spAutoFit));
else
xPropertySet->setPropertyValue(
u"TextAutoGrowHeight"_ustr,
uno::Any(getBaseToken(nElementToken) == XML_spAutoFit));
}
}
break;
case XML_prstTxWarp:
if (rAttribs.hasAttribute(XML_prst))
{
uno::Reference<beans::XPropertySet> xPropertySet(mxShape, uno::UNO_QUERY);
if (xPropertySet.is())
{
std::optional<OUString> presetShapeName = rAttribs.getString(XML_prst);
const OUString& preset = presetShapeName.value();
comphelper::SequenceAsHashMap aCustomShapeGeometry(
xPropertySet->getPropertyValue(u"CustomShapeGeometry"_ustr));
aCustomShapeGeometry[u"PresetTextWarp"_ustr] <<= preset;
xPropertySet->setPropertyValue(
u"CustomShapeGeometry"_ustr,
uno::Any(aCustomShapeGeometry.getAsConstPropertyValueList()));
}
}
return new oox::drawingml::PresetTextShapeContext(
*this, rAttribs, *(getShape()->getCustomShapeProperties()));
case XML_txbx:
{
mpShapePtr->getCustomShapeProperties()->setShapeTypeOverride(true);
mpShapePtr->setTextBox(true);
//in case if the textbox is linked, save the attributes
//for further processing.
if (rAttribs.hasAttribute(XML_id))
{
std::optional<OUString> id = rAttribs.getString(XML_id);
if (id.has_value())
{
oox::drawingml::LinkedTxbxAttr linkedTxtBoxAttr;
linkedTxtBoxAttr.id = id.value().toInt32();
mpShapePtr->setTxbxHasLinkedTxtBox(true);
mpShapePtr->setLinkedTxbxAttributes(linkedTxtBoxAttr);
}
}
return this;
}
break;
case XML_linkedTxbx:
{
//in case if the textbox is linked, save the attributes
//for further processing.
mpShapePtr->getCustomShapeProperties()->setShapeTypeOverride(true);
mpShapePtr->setTextBox(true);
std::optional<OUString> id = rAttribs.getString(XML_id);
std::optional<OUString> seq = rAttribs.getString(XML_seq);
if (id.has_value() && seq.has_value())
{
oox::drawingml::LinkedTxbxAttr linkedTxtBoxAttr;
linkedTxtBoxAttr.id = id.value().toInt32();
linkedTxtBoxAttr.seq = seq.value().toInt32();
mpShapePtr->setTxbxHasLinkedTxtBox(true);
mpShapePtr->setLinkedTxbxAttributes(linkedTxtBoxAttr);
}
}
break;
default:
return ShapeContext::onCreateContext(nElementToken, rAttribs);
}
return nullptr;
}
void WpsContext::onEndElement()
{
// Convert shape to Fontwork shape if necessary and meaningful.
// Only at end of bodyPr all needed info is available.
if (getBaseToken(getCurrentElement()) != XML_bodyPr)
return;
// Make sure all needed parts are available
auto* pCustomShape
= dynamic_cast<SdrObjCustomShape*>(SdrObject::getSdrObjectFromXShape(mxShape));
if (!pCustomShape || !mpShapePtr || !mxShape.is())
return;
uno::Reference<beans::XPropertySet> xShapePropertySet(mxShape, uno::UNO_QUERY);
if (!xShapePropertySet.is())
return;
// This is the text in the frame, associated with the shape
uno::Reference<text::XText> xText(mxShape, uno::UNO_QUERY);
if (!xText.is())
return;
OUString sMSPresetType;
comphelper::SequenceAsHashMap aCustomShapeGeometry(
xShapePropertySet->getPropertyValue(u"CustomShapeGeometry"_ustr));
aCustomShapeGeometry[u"PresetTextWarp"_ustr] >>= sMSPresetType;
if (sMSPresetType.isEmpty() || sMSPresetType == u"textNoShape")
return;
// Word can combine its "abc Transform" with a lot of shape types. LibreOffice can only render
// the old kind WordArt, which is based on a rectangle. In case of non rectangular shape we keep
// the shape and do not convert the text to Fontwork.
OUString sType;
aCustomShapeGeometry[u"Type"_ustr] >>= sType;
if (sType != u"ooxml-rect")
return;
// Copy properties from frame text to have them available after the frame is removed.
std::vector<beans::PropertyValue> aTextPropVec;
if (!lcl_getTextPropsFromFrameText(xText, aTextPropVec))
return;
comphelper::SequenceAsHashMap aTextPropMap(comphelper::containerToSequence(aTextPropVec));
// Copy text content from frame to shape. Since Fontwork uses simple text anyway, we can use
// a string.
OUString sFrameContent(xText->getString());
pCustomShape->NbcSetText(sFrameContent);
// Setting the property "TextBox" to false includes removing the attached frame from the shape.
xShapePropertySet->setPropertyValue(u"TextBox"_ustr, uno::Any(false));
// Set the shape into text path mode, so that the text is drawn as Fontwork. Word renders a legacy
// "text on path" without the legacy stretching, therefore use false for bFromWordArt.
mpShapePtr->getCustomShapeProperties()->setShapeTypeOverride(true);
FontworkHelpers::putCustomShapeIntoTextPathMode(mxShape, getShape()->getCustomShapeProperties(),
sMSPresetType, /*bFromWordArt*/ false);
// Apply the text props to the fontwork shape
lcl_setTextPropsToShape(xShapePropertySet, aTextPropVec); // includes e.g. FontName
lcl_setTextAnchorFromTextProps(xShapePropertySet, aTextPropMap);
// Fontwork in LO uses fill and stroke of the shape and cannot style text portions individually.
// "abc Transform" in Word uses fill and outline of the characters.
// We need to copy the properties from a run to the shape.
oox::drawingml::ShapePropertyMap aStrokeShapeProps(getFilter().getModelObjectHelper());
oox::drawingml::LineProperties aCreatedLineProperties
= lcl_generateLinePropertiesFromTextProps(aTextPropMap);
aCreatedLineProperties.pushToPropMap(aStrokeShapeProps, getFilter().getGraphicHelper());
lcl_applyShapePropsToShape(xShapePropertySet, aStrokeShapeProps);
oox::drawingml::ShapePropertyMap aFillShapeProps(getFilter().getModelObjectHelper());
oox::drawingml::FillProperties aCreatedFillProperties
= lcl_generateFillPropertiesFromTextProps(aTextPropMap);
aCreatedFillProperties.pushToPropMap(aFillShapeProps, getFilter().getGraphicHelper(),
/*nShapeRotation*/ 0,
/*nPhClr*/ API_RGB_TRANSPARENT,
/*aShapeSize*/ css::awt::Size(0, 0), /*nPhClrTheme*/ -1,
pCustomShape->IsMirroredX(), pCustomShape->IsMirroredY(),
/*bIsCustomShape*/ true);
lcl_applyShapePropsToShape(xShapePropertySet, aFillShapeProps);
// Copying the text content from frame to shape as string has lost the styles. Apply the used text
// properties back to all runs in the text.
uno::Reference<text::XText> xNewText(pCustomShape->getUnoShape(), uno::UNO_QUERY);
if (xNewText.is())
lcl_applyUsedTextPropsToAllTextRuns(xNewText, aTextPropVec);
// Fontwork stretches the text to the given path. So adapt shape size to text is nonsensical.
xShapePropertySet->setPropertyValue(u"TextAutoGrowHeight"_ustr, uno::Any(false));
xShapePropertySet->setPropertyValue(u"TextAutoGrowWidth"_ustr, uno::Any(false));
}
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
↑ V614 Uninitialized variable 'nRet' used.
↑ V785 Constant expression in switch statement.
↑ V1019 Compound assignment expression is used inside condition.
↑ V1019 Compound assignment expression 'aProp.Value >>= aTextOutlineSeq' is used inside condition.
↑ V1019 Compound assignment expression 'aProp.Value >>= aTextFillSeq' is used inside condition.