/* -*- 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace com::sun::star; namespace { bool lcl_getTextPropsFromFrameText(const uno::Reference& xText, std::vector& rTextPropVec) { if (!xText.is()) return false; uno::Reference xTextCursor = xText->createTextCursor(); xTextCursor->gotoStart(false); xTextCursor->gotoEnd(true); uno::Reference paraEnumAccess(xText, uno::UNO_QUERY); if (!paraEnumAccess.is()) return false; uno::Reference paraEnum(paraEnumAccess->createEnumeration()); while (paraEnum->hasMoreElements()) { uno::Reference xParagraph(paraEnum->nextElement(), uno::UNO_QUERY); uno::Reference runEnumAccess(xParagraph, uno::UNO_QUERY); if (!runEnumAccess.is()) continue; uno::Reference runEnum = runEnumAccess->createEnumeration(); while (runEnum->hasMoreElements()) { uno::Reference xRun(runEnum->nextElement(), uno::UNO_QUERY); if (xRun->getString().isEmpty()) continue; uno::Reference 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& aPropertyValueAsSeq, const OUString& rName, OUString& rValue) { comphelper::SequenceAsHashMap aPropertyValueAsMap(aPropertyValueAsSeq); uno::Sequence 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& rPropertyValueAsSeq, const OUString& rName, sal_Int32& rValue) { comphelper::SequenceAsHashMap aPropertyValueAsMap(rPropertyValueAsSeq); uno::Sequence 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& 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 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& rPropSeq, oox::drawingml::Color& rColor) { bool bColorFound = false; comphelper::SequenceAsHashMap aPropMap(rPropSeq); uno::Sequence 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& 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 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 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 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 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 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& 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 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 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 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 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 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(fTint + 0.5)); double fOff = oox::drawingml::MAX_PERCENT - fTint; aFillProperties.maFillColor.addTransformation(OOX_TOKEN(w14, lumOff), static_cast(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(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& xShapePropertySet, const oox::drawingml::ShapePropertyMap& rShapeProps) { for (const auto& rProp : rShapeProps.makePropertyValueSequence()) { xShapePropertySet->setPropertyValue(rProp.Name, rProp.Value); } } void lcl_setTextAnchorFromTextProps(const uno::Reference& 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& xShapePropertySet, std::vector& 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& xText, const std::vector& aTextPropVec) { if (!xText.is()) return; uno::Reference xTextCursor = xText->createTextCursor(); xTextCursor->gotoStart(false); xTextCursor->gotoEnd(true); uno::Reference paraEnumAccess(xText, uno::UNO_QUERY); if (!paraEnumAccess.is()) return; uno::Reference paraEnum(paraEnumAccess->createEnumeration()); while (paraEnum->hasMoreElements()) { uno::Reference xParagraph(paraEnum->nextElement(), uno::UNO_QUERY); uno::Reference runEnumAccess(xParagraph, uno::UNO_QUERY); if (!runEnumAccess.is()) continue; uno::Reference runEnum = runEnumAccess->createEnumeration(); while (runEnum->hasMoreElements()) { uno::Reference xRun(runEnum->nextElement(), uno::UNO_QUERY); if (xRun->getString().isEmpty()) continue; uno::Reference 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 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(&rParent)) m_bHasWPGParent = pParent->isFullWPGSupport(); else if (dynamic_cast(&rParent)) m_bHasWPGParent = true; else m_bHasWPGParent = false; if ((pMasterShapePtr && pMasterShapePtr->isInWordprocessingCanvas()) || dynamic_cast(&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( 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 xServiceInfo(mxShape, uno::UNO_QUERY); uno::Reference 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(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 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)); } auto xPropertySetInfo = xPropertySet->getPropertySetInfo(); if (xServiceInfo.is()) { // Handle inset attributes for Writer textframes. sal_Int32 aInsets[] = { XML_lIns, XML_tIns, XML_rIns, XML_bIns }; std::optional oInsets[4]; for (std::size_t i = 0; i < SAL_N_ELEMENTS(aInsets); ++i) { std::optional 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]) continue; if (xPropertySetInfo && xPropertySetInfo->hasPropertyByName(aShapeProps[i])) xPropertySet->setPropertyValue(aShapeProps[i], uno::Any(*oInsets[i])); else SAL_WARN("oox", "Property: " << aShapeProps[i] << " not supported"); } } // Handle text vertical adjustment inside a text frame if (rAttribs.hasAttribute(XML_anchor)) { if (xPropertySetInfo && xPropertySetInfo->hasPropertyByName(u"TextVerticalAdjust"_ustr)) { drawing::TextVerticalAdjust eAdjust = drawingml::GetTextVerticalAdjust( rAttribs.getToken(XML_anchor, XML_t)); xPropertySet->setPropertyValue(u"TextVerticalAdjust"_ustr, uno::Any(eAdjust)); } else SAL_WARN("oox", "Property: TextVerticalAdjust not supported"); } // Apply character color of the shape to the shape's textbox. uno::Reference xText(mxShape, uno::UNO_QUERY); if (xText) { 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 paraEnumAccess{ xText, uno::UNO_QUERY }) { uno::Reference paraEnum( paraEnumAccess->createEnumeration()); while (paraEnum->hasMoreElements()) { uno::Reference xParagraph(paraEnum->nextElement(), uno::UNO_QUERY); uno::Reference runEnumAccess( xParagraph, uno::UNO_QUERY); if (!runEnumAccess.is()) continue; if (uno::Reference xParaPropSet{ xParagraph, uno::UNO_QUERY }) if ((xParaPropSet->getPropertyValue(u"ParaBackColor"_ustr) >>= aColor) && aColor != COL_AUTO) continue; uno::Reference runEnum = runEnumAccess->createEnumeration(); while (runEnum->hasMoreElements()) { uno::Reference xRun(runEnum->nextElement(), uno::UNO_QUERY); const uno::Reference xRunState( xRun, uno::UNO_QUERY); if (!xRunState || xRunState->getPropertyState(u"CharColor"_ustr) == beans::PropertyState_DEFAULT_VALUE) { uno::Reference 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); } } } } } } if (xPropertySetInfo && xPropertySetInfo->hasPropertyByName(u"TextWordWrap"_ustr)) { auto nWrappingType = rAttribs.getToken(XML_wrap, XML_square); xPropertySet->setPropertyValue(u"TextWordWrap"_ustr, uno::Any(nWrappingType == XML_square)); } else SAL_WARN("oox", "Property: TextWordWrap not supported"); return this; } else if (m_bHasWPGParent && mpShapePtr) { // this WPS context has to be inside a WPG shape, so the 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 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 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 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 { auto xPropertySetInfo = xPropertySet->getPropertySetInfo(); if (xPropertySetInfo && xPropertySetInfo->hasPropertyByName(u"TextAutoGrowHeight"_ustr)) { xPropertySet->setPropertyValue( u"TextAutoGrowHeight"_ustr, uno::Any(getBaseToken(nElementToken) == XML_spAutoFit)); } else SAL_WARN("oox", "Property: TextAutoGrowHeight not supported"); } } } break; case XML_prstTxWarp: if (rAttribs.hasAttribute(XML_prst)) { uno::Reference xPropertySet(mxShape, uno::UNO_QUERY); if (xPropertySet.is()) { std::optional 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 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 id = rAttribs.getString(XML_id); std::optional 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(SdrObject::getSdrObjectFromXShape(mxShape)); if (!pCustomShape || !mpShapePtr || !mxShape.is()) return; uno::Reference xShapePropertySet(mxShape, uno::UNO_QUERY); if (!xShapePropertySet.is()) return; // This is the text in the frame, associated with the shape uno::Reference 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 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 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: */