/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * 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/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #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 #include #include #include #include #include using namespace com::sun::star; void FontworkHelpers::resetPropertyValueInVec(std::vector& rPropVec, const OUString& rName) { auto aIterator = std::find_if( rPropVec.begin(), rPropVec.end(), [rName](const beans::PropertyValue& rValue) { return rValue.Name == rName; }); if (aIterator != rPropVec.end()) rPropVec.erase(aIterator); } void FontworkHelpers::putCustomShapeIntoTextPathMode( const css::uno::Reference& xShape, const oox::drawingml::CustomShapePropertiesPtr& pCustomShapePropertiesPtr, const OUString& sMSPresetType, const bool bFromWordArt) { if (!xShape.is() || !pCustomShapePropertiesPtr || sMSPresetType == u"textNoShape") return; uno::Reference xDefaulter(xShape, uno::UNO_QUERY); if (!xDefaulter.is()) return; uno::Reference xSet(xShape, uno::UNO_QUERY); if (!xSet.is()) return; // The DrawingML shapes from the presetTextWarpDefinitions are mapped to the definitions // in svx/../EnhancedCustomShapeGeometry.cxx, which are used for WordArt shapes from // binary MS Office. Therefore all adjustment values need to be adapted. const OUString sFontworkType = PresetGeometryTypeNames::GetFontworkType(sMSPresetType); auto aAdjGdList = pCustomShapePropertiesPtr->getAdjustmentGuideList(); uno::Sequence aAdjustment( !aAdjGdList.empty() ? aAdjGdList.size() : 1); auto pAdjustment = aAdjustment.getArray(); int nIndex = 0; for (const auto& aEntry : aAdjGdList) { double fValue = aEntry.maFormula.toDouble(); // then: polar-handle, else: XY-handle // There exist only 8 polar-handles at all in presetTextWarp. if ((sFontworkType == "fontwork-arch-down-curve") || (sFontworkType == "fontwork-arch-down-pour" && aEntry.maName == "adj1") || (sFontworkType == "fontwork-arch-up-curve") || (sFontworkType == "fontwork-arch-up-pour" && aEntry.maName == "adj1") || (sFontworkType == "fontwork-open-circle-curve") || (sFontworkType == "fontwork-open-circle-pour" && aEntry.maName == "adj1") || (sFontworkType == "fontwork-circle-curve") || (sFontworkType == "fontwork-circle-pour" && aEntry.maName == "adj1")) { // DrawingML has 1/60000 degree unit, but WordArt simple degree. Range [0..360[ // or range ]-180..180] doesn't matter, because only cos(angle) and // sin(angle) are used. fValue = NormAngle360(fValue / 60000.0); } else { // DrawingML writes adjustment guides as relative value with 100% = 100000, // but WordArt definitions use values absolute in viewBox 0 0 21600 21600, // so scale with 21600/100000 = 0.216, with two exceptions: // X-handles of waves describe increase/decrease relative to horizontal center. // The gdRefR of pour-shapes is not relative to viewBox but to radius. if ((sFontworkType == "mso-spt158" && aEntry.maName == "adj2") // textDoubleWave1 || (sFontworkType == "fontwork-wave" && aEntry.maName == "adj2") // textWave1 || (sFontworkType == "mso-spt157" && aEntry.maName == "adj2") // textWave2 || (sFontworkType == "mso-spt159" && aEntry.maName == "adj2")) // textWave4 { fValue = (fValue + 50000.0) * 0.216; } else if ((sFontworkType == "fontwork-arch-down-pour" && aEntry.maName == "adj2") || (sFontworkType == "fontwork-arch-up-pour" && aEntry.maName == "adj2") || (sFontworkType == "fontwork-open-circle-pour" && aEntry.maName == "adj2") || (sFontworkType == "fontwork-circle-pour" && aEntry.maName == "adj2")) { fValue *= 0.108; } else { fValue *= 0.216; } } pAdjustment[nIndex].Value <<= fValue; pAdjustment[nIndex++].State = css::beans::PropertyState_DIRECT_VALUE; } // Set attributes in CustomShapeGeometry xDefaulter->createCustomShapeDefaults(sFontworkType); auto aGeomPropSeq = xSet->getPropertyValue("CustomShapeGeometry").get>(); auto aGeomPropVec = comphelper::sequenceToContainer>(aGeomPropSeq); // Reset old properties static constexpr OUString sTextPath(u"TextPath"_ustr); static constexpr OUString sAdjustmentValues(u"AdjustmentValues"_ustr); static constexpr OUString sPresetTextWarp(u"PresetTextWarp"_ustr); resetPropertyValueInVec(aGeomPropVec, u"CoordinateSize"_ustr); resetPropertyValueInVec(aGeomPropVec, u"Equations"_ustr); resetPropertyValueInVec(aGeomPropVec, u"Path"_ustr); resetPropertyValueInVec(aGeomPropVec, sAdjustmentValues); resetPropertyValueInVec(aGeomPropVec, u"ViewBox"_ustr); resetPropertyValueInVec(aGeomPropVec, u"Handles"_ustr); resetPropertyValueInVec(aGeomPropVec, sTextPath); resetPropertyValueInVec(aGeomPropVec, sPresetTextWarp); bool bScaleX(false); if (!bFromWordArt && (sMSPresetType == u"textArchDown" || sMSPresetType == u"textArchUp" || sMSPresetType == u"textCircle" || sMSPresetType == u"textButton")) { bScaleX = true; } // Apply new properties uno::Sequence aPropertyValues(comphelper::InitPropertySequence( { { sTextPath, uno::Any(true) }, { u"TextPathMode"_ustr, uno::Any(drawing::EnhancedCustomShapeTextPathMode_PATH) }, { u"ScaleX"_ustr, uno::Any(bScaleX) } })); aGeomPropVec.push_back(comphelper::makePropertyValue(sTextPath, aPropertyValues)); aGeomPropVec.push_back(comphelper::makePropertyValue(sPresetTextWarp, sMSPresetType)); if (!aAdjGdList.empty()) { aGeomPropVec.push_back(comphelper::makePropertyValue(sAdjustmentValues, aAdjustment)); } xSet->setPropertyValue(u"CustomShapeGeometry"_ustr, uno::Any(comphelper::containerToSequence(aGeomPropVec))); } OString FontworkHelpers::GetVMLFontworkShapetypeMarkup(const MSO_SPT eShapeType) { // The markup is taken from VML in DOCX documents. Using the generated 'vml-shape-types' file // does not work. static const std::map aTypeToMarkupMap{ { mso_sptTextSimple, "" }, { mso_sptTextOctagon, "" }, { mso_sptTextHexagon, "" }, { mso_sptTextCurve, "" }, { mso_sptTextWave, "" }, { mso_sptTextRing, "" }, { mso_sptTextOnCurve, "" }, { mso_sptTextOnRing, "" }, { mso_sptTextPlainText, "" }, { mso_sptTextStop, "" }, { mso_sptTextTriangle, "" }, { mso_sptTextTriangleInverted, "" }, { mso_sptTextChevron, "" }, { mso_sptTextChevronInverted, "" }, { mso_sptTextRingInside, "" }, { mso_sptTextRingOutside, "" }, { mso_sptTextArchUpCurve, "" }, { mso_sptTextArchDownCurve, "" }, { mso_sptTextCircleCurve, "" }, { mso_sptTextButtonCurve, "" }, { mso_sptTextArchUpPour, "" }, { mso_sptTextArchDownPour, "" }, { mso_sptTextCirclePour, "" }, { mso_sptTextButtonPour, "" }, { mso_sptTextCurveUp, "" }, { mso_sptTextCurveDown, "" }, { mso_sptTextCascadeUp, "" }, { mso_sptTextCascadeDown, "" }, { mso_sptTextWave1, "" }, { mso_sptTextWave2, "" }, { mso_sptTextWave3, "" }, { mso_sptTextWave4, "" }, { mso_sptTextInflate, "" }, { mso_sptTextDeflate, "" }, { mso_sptTextInflateBottom, "" }, { mso_sptTextDeflateBottom, "" }, { mso_sptTextInflateTop, "" }, { mso_sptTextDeflateTop, "" }, { mso_sptTextDeflateInflate, "" }, { mso_sptTextDeflateInflateDeflate, "" }, { mso_sptTextFadeRight, "" }, { mso_sptTextFadeLeft, "" }, { mso_sptTextFadeUp, "" }, { mso_sptTextFadeDown, "" }, { mso_sptTextSlantUp, "" }, { mso_sptTextSlantDown, "" }, { mso_sptTextCanUp, "" }, { mso_sptTextCanDown, "" } }; auto i(aTypeToMarkupMap.find(eShapeType)); return i == aTypeToMarkupMap.end() ? OString() : i->second; } void FontworkHelpers::collectCharColorProps(const uno::Reference& rXText, std::vector& rCharPropVec) { if (!rXText.is()) return; uno::Reference rXTextCursor = rXText->createTextCursor(); rXTextCursor->gotoStart(false); rXTextCursor->gotoEnd(true); uno::Reference paraEnumAccess(rXText, 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; // We have found a non-empty run. Collect its simple color properties. const std::array aNamesArray = { u"CharColor"_ustr, u"CharLumMod"_ustr, u"CharLumOff"_ustr, u"CharColorTheme"_ustr, u"CharComplexColor"_ustr, u"CharTransparence"_ustr }; for (const auto& propName : aNamesArray) { if (xRunPropSetInfo->hasPropertyByName(propName)) rCharPropVec.push_back(comphelper::makePropertyValue( propName, xRunPropSet->getPropertyValue(propName))); } return; } } } void FontworkHelpers::applyPropsToRuns(const std::vector& rTextPropVec, uno::Reference& rXText) { if (!rXText.is()) return; uno::Reference xTextCursor = rXText->createTextCursor(); xTextCursor->gotoStart(false); xTextCursor->gotoEnd(true); uno::Reference paraEnumAccess(rXText, 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); uno::Reference xRunPropSet(xRun, uno::UNO_QUERY); if (!xRunPropSet.is()) continue; auto xRunPropSetInfo = xRunPropSet->getPropertySetInfo(); if (!xRunPropSetInfo.is()) continue; for (const beans::PropertyValue& rProp : rTextPropVec) { if (xRunPropSetInfo->hasPropertyByName(rProp.Name) && !(xRunPropSetInfo->getPropertyByName(rProp.Name).Attributes & beans::PropertyAttribute::READONLY) && rProp.Name != u"CharInteropGrabBag") { xRunPropSet->setPropertyValue(rProp.Name, rProp.Value); } } } } } namespace { constexpr const std::array aCharPropNames{ u"CharColorLumMod", u"CharColorLumOff", u"CharColorTheme", u"CharComplexColor", u"CharTransparence" }; constexpr const std::array aShapePropNames{ u"FillColorLumMod", u"FillColorLumOff", u"FillColorTheme", u"FillComplexColor", u"FillTransparence" }; } void FontworkHelpers::createCharFillPropsFromShape( const uno::Reference& rXPropSet, std::vector& rCharPropVec) { auto xPropSetInfo = rXPropSet->getPropertySetInfo(); if (!xPropSetInfo.is()) return; // CharColor contains the color including all color transformations // FillColor contains darken and lighten but not transparency sal_Int32 nColorRGB = 0; if (xPropSetInfo->hasPropertyByName(u"FillColor"_ustr) && (rXPropSet->getPropertyValue(u"FillColor"_ustr) >>= nColorRGB)) { ::Color aColor(ColorTransparency, nColorRGB); sal_Int16 nTransPercent = 0; if (xPropSetInfo->hasPropertyByName(u"FillTransparence"_ustr) && (rXPropSet->getPropertyValue(u"FillTransparence"_ustr) >>= nTransPercent)) { sal_uInt8 nAlpha = 255 - sal_uInt8(std::lround(double(nTransPercent) * 2.55)); aColor.SetAlpha(nAlpha); } rCharPropVec.push_back(comphelper::makePropertyValue(u"CharColor"_ustr, sal_Int32(aColor))); } for (size_t i = 0; i < 5; i++) { OUString aPropertyName(aShapePropNames[i]); if (xPropSetInfo->hasPropertyByName(aPropertyName)) rCharPropVec.push_back(comphelper::makePropertyValue( OUString(aCharPropNames[i]), rXPropSet->getPropertyValue(aPropertyName))); } } bool FontworkHelpers::createPrstDashFromLineDash(const drawing::LineDash& rLineDash, const drawing::LineCap& rLineCap, OUString& rsPrstDash) { bool bIsConverted = false; bool bIsRelative(rLineDash.Style == drawing::DashStyle_RECTRELATIVE || rLineDash.Style == drawing::DashStyle_ROUNDRELATIVE); if (bIsRelative && rLineDash.Dots == 1) { // The length were tweaked on import in case of prstDash. Revert it here. sal_uInt32 nDotLen = rLineDash.DotLen; sal_uInt32 nDashLen = rLineDash.DashLen; sal_uInt32 nDistance = rLineDash.Distance; if (rLineCap != drawing::LineCap_BUTT && nDistance >= 99) { nDistance -= 99; nDotLen += 99; if (nDashLen > 0) nDashLen += 99; } // LO uses length 0 for 100%, if the attribute is missing in ODF. // Other applications might write 100%. Make is unique for the conditions. if (nDotLen == 0) nDotLen = 100; if (nDashLen == 0 && rLineDash.Dashes > 0) nDashLen = 100; bIsConverted = true; if (nDotLen == 100 && rLineDash.Dashes == 0 && nDashLen == 0 && nDistance == 300) rsPrstDash = u"dot"_ustr; else if (nDotLen == 400 && rLineDash.Dashes == 0 && nDashLen == 0 && nDistance == 300) rsPrstDash = u"dash"_ustr; else if (nDotLen == 400 && rLineDash.Dashes == 1 && nDashLen == 100 && nDistance == 300) rsPrstDash = u"dashDot"_ustr; else if (nDotLen == 800 && rLineDash.Dashes == 0 && nDashLen == 0 && nDistance == 300) rsPrstDash = u"lgDash"_ustr; else if (nDotLen == 800 && rLineDash.Dashes == 1 && nDashLen == 100 && nDistance == 300) rsPrstDash = u"lgDashDot"_ustr; else if (nDotLen == 800 && rLineDash.Dashes == 2 && nDashLen == 100 && nDistance == 300) rsPrstDash = u"lgDashDotDot"_ustr; else if (nDotLen == 100 && rLineDash.Dashes == 0 && nDashLen == 0 && nDistance == 100) rsPrstDash = u"sysDot"_ustr; else if (nDotLen == 300 && rLineDash.Dashes == 0 && nDashLen == 0 && nDistance == 100) rsPrstDash = u"sysDash"_ustr; else if (nDotLen == 300 && rLineDash.Dashes == 1 && nDashLen == 100 && nDistance == 100) rsPrstDash = u"sysDashDot"_ustr; else if (nDotLen == 300 && rLineDash.Dashes == 2 && nDashLen == 100 && nDistance == 100) rsPrstDash = "sysDashDotDot"; else bIsConverted = false; } return bIsConverted; } bool FontworkHelpers::getThemeColorFromShape( OUString const& rPropertyName, const uno::Reference& xPropertySet, model::ComplexColor& rComplexColor) { auto xPropSetInfo = xPropertySet->getPropertySetInfo(); if (!xPropSetInfo.is()) return false; uno::Reference xComplexColor; if (xPropSetInfo->hasPropertyByName(rPropertyName) && (xPropertySet->getPropertyValue(rPropertyName) >>= xComplexColor) && xComplexColor.is()) { rComplexColor = model::color::getFromXComplexColor(xComplexColor); return rComplexColor.isValidThemeType(); } return false; } namespace { // Contains information about one gradient stop. Each gradient has at least 2 of these. struct GradientStopColor { // RGBColor contains no transformations. In case TTColor has other type than // ThemeColorType::Unknown, it has precedence. The color transformations in TTColor are used // for RGBColor as well. model::ComplexColor TTColor; // ThemeColorType and color transformations ::Color RGBColor; }; } // 'first' contains the position in the range 0 (=0%) to 100000 (=100%) in the gradient as needed for // the 'pos' attribute in element in oox, 'second' contains color and color transformations // at this position. The map contains all information needed for a element in oox. typedef std::multimap ColorMapType; namespace { constexpr const std::array W14ColorNames{ u"tx1", u"bg1", u"tx2", u"bg2", u"accent1", u"accent2", u"accent3", u"accent4", u"accent5", u"accent6", u"hlink", u"folHlink" }; constexpr const std::array WColorNames{ u"text1", u"background1", u"text2", u"background2", u"accent1", u"accent2", u"accent3", u"accent4", u"accent5", u"accent6", u"hyperlink", u"followedHyperlink" }; // Returns the string to be used in w14:schemeClr in case of w14:textOutline or w14:textFill OUString lcl_getW14MarkupStringForThemeColor(const model::ComplexColor& rComplexColor) { const sal_uInt8 nClrNameIndex = std::clamp( sal_Int32(rComplexColor.getThemeColorType()), sal_Int32(model::ThemeColorType::Dark1), sal_Int32(model::ThemeColorType::FollowedHyperlink)); return OUString(W14ColorNames[nClrNameIndex]); } // Returns the string to be used in w:themeColor. It is exported via CharThemeColor. OUString lcl_getWMarkupStringForThemeColor(const model::ComplexColor& rComplexColor) { const sal_uInt8 nClrNameIndex = std::clamp( sal_Int32(rComplexColor.getThemeColorType()), sal_Int32(model::ThemeColorType::Dark1), sal_Int32(model::ThemeColorType::FollowedHyperlink)); return OUString(WColorNames[nClrNameIndex]); } // Puts the value of the first occurrence of rType in rComplexColor into rValue and returns true. // If such does not exist, rValue is unchanged and the method returns false. bool lcl_getThemeColorTransformationValue(const model::ComplexColor& rComplexColor, const model::TransformationType& rType, sal_Int16& rValue) { const std::vector aTransVec(rComplexColor.getTransformations()); auto bItemFound = [rType](const model::Transformation& rTrans) { return rType == rTrans.meType; }; auto pIt = std::find_if(aTransVec.begin(), aTransVec.end(), bItemFound); if (pIt == aTransVec.end()) return false; rValue = (*pIt).mnValue; return true; } // Adds the child elements 'lumMod' and 'lumOff' to 'schemeClr' maCurrentElement of pGrabStack, // if such exist in rComplexColor. 'alpha' is contained in the maTransformations of rComplexColor // in case of gradient fill. void lcl_addColorTransformationToGrabBagStack(const model::ComplexColor& rComplexColor, std::unique_ptr& pGrabBagStack) { if (pGrabBagStack == nullptr) return; for (auto const& rColorTransform : rComplexColor.getTransformations()) { switch (rColorTransform.meType) { case model::TransformationType::LumMod: pGrabBagStack->push("lumMod"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("val", rColorTransform.mnValue * 10); pGrabBagStack->pop(); pGrabBagStack->pop(); break; case model::TransformationType::LumOff: pGrabBagStack->push("lumOff"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("val", rColorTransform.mnValue * 10); pGrabBagStack->pop(); pGrabBagStack->pop(); break; case model::TransformationType::Alpha: pGrabBagStack->push("alpha"); pGrabBagStack->push("attributes"); // model::TransformationType::Alpha is designed to be used with a:alpha, which has // opacity. But w14:alpha uses transparency. So convert it here. pGrabBagStack->addInt32("val", oox::drawingml::MAX_PERCENT - rColorTransform.mnValue * 10); pGrabBagStack->pop(); pGrabBagStack->pop(); break; default: // other child elements can be added later if needed for Fontwork break; } } } void lcl_getGradientsFromShape(const uno::Reference& rXPropSet, const uno::Reference& rXPropSetInfo, awt::Gradient2& rColorGradient, bool& rbHasColorGradient, awt::Gradient2& rTransparenceGradient, bool& rbHasTransparenceGradient) { OUString sColorGradientName; rbHasColorGradient = rXPropSetInfo->hasPropertyByName(u"FillGradientName"_ustr) && (rXPropSet->getPropertyValue(u"FillGradientName"_ustr) >>= sColorGradientName) && !sColorGradientName.isEmpty() && rXPropSetInfo->hasPropertyByName(u"FillGradient"_ustr) && (rXPropSet->getPropertyValue(u"FillGradient"_ustr) >>= rColorGradient); OUString sTransparenceGradientName; rbHasTransparenceGradient = rXPropSetInfo->hasPropertyByName(u"FillTransparenceGradientName"_ustr) && (rXPropSet->getPropertyValue(u"FillTransparenceGradientName"_ustr) >>= sTransparenceGradientName) && !sTransparenceGradientName.isEmpty() && rXPropSetInfo->hasPropertyByName(u"FillTransparenceGradient"_ustr) && (rXPropSet->getPropertyValue(u"FillTransparenceGradient"_ustr) >>= rTransparenceGradient); } ColorMapType lcl_createColorMapFromShapeProps( const uno::Reference& rXPropSet, const uno::Reference& rXPropSetInfo, const awt::Gradient2& rColorGradient, const bool& rbHasColorGradient, const awt::Gradient2& rTransparenceGradient, const bool& rbHasTransparenceGradient) { // LibreOffice can use color gradients and transparency gradients with different geometries. // That is not possible in OOXML, so a fill might look different in Word. But a round-trip // with gradients imported from Word, should work well. // Word has transparency not as separate gradient but as color transformation in a color // gradient. Thus we synchronize the gradients. Then they have same offsets and count. basegfx::BColor aSingleColor; basegfx::BGradient aColorBGradient; basegfx::BColorStops aColorStops; if (rbHasColorGradient) { aColorBGradient = model::gradient::getFromUnoGradient2(rColorGradient); aColorBGradient.tryToApplyStartEndIntensity(); aColorBGradient.tryToApplyBorder(); aColorBGradient.tryToApplyAxial(); basegfx::utils::prepareColorStops(aColorBGradient, aColorStops, aSingleColor); // All gradient styles but LINEAR and AXIAL (which is already converted to LINEAR) need the // stops sequence reverse. if (awt::GradientStyle_LINEAR != aColorBGradient.GetGradientStyle()) aColorStops.reverseColorStops(); } else { sal_Int32 nFillColor(0); if (rXPropSetInfo->hasPropertyByName("FillColor")) rXPropSet->getPropertyValue(u"FillColor"_ustr) >>= nFillColor; aSingleColor = ::Color(ColorTransparency, nFillColor).getBColor().clamp(); } basegfx::BColor aSingleTrans; basegfx::BGradient aTransBGradient; basegfx::BColorStops aTransStops; if (rbHasTransparenceGradient) { aTransBGradient = model::gradient::getFromUnoGradient2(rTransparenceGradient); aTransBGradient.tryToApplyStartEndIntensity(); // usually 100%, but might be set by macro aTransBGradient.tryToApplyBorder(); aTransBGradient.tryToApplyAxial(); basegfx::utils::prepareColorStops(aTransBGradient, aTransStops, aSingleTrans); // All gradient styles but LINEAR and AXIAL (which is already converted to LINEAR) need the // stops sequence reverse. if (awt::GradientStyle_LINEAR != aTransBGradient.GetGradientStyle()) aTransStops.reverseColorStops(); } else { sal_Int16 nAPITrans(0); if (rXPropSetInfo->hasPropertyByName(u"FillTransparence"_ustr)) rXPropSet->getPropertyValue(u"FillTransparence"_ustr) >>= nAPITrans; // API transparency is in range 0..100, BColor in range [0.0, 1.0]. aSingleTrans = basegfx::BColor(nAPITrans * 0.01).clamp(); } basegfx::utils::synchronizeColorStops(aColorStops, aTransStops, aSingleColor, aSingleTrans); ColorMapType aColorMap; // If we have no color gradient, the fix fill color might be a theme color. In that case we use // it instead of the color from the color stop. GradientStopColor aFixColor; bool bUseThemeColor(!rbHasColorGradient && FontworkHelpers::getThemeColorFromShape("FillComplexColor", rXPropSet, aFixColor.TTColor)); for (auto itC = aColorStops.begin(), itT = aTransStops.begin(); itC != aColorStops.end() && itT != aTransStops.end(); ++itC, ++itT) { GradientStopColor aNextStopColor = aFixColor; if (!bUseThemeColor) { aNextStopColor.TTColor = model::ComplexColor(); aNextStopColor.RGBColor = ::Color((*itC).getStopColor()); } // model::TransformationType::Alpha is opacity in range 0..10000, // BColor is transparency in range [0.0, 1.0] sal_Int16 nAlpha = std::clamp( 10000 - std::lround((*itT).getStopColor().luminance() * 10000.0), 0, 10000); if (nAlpha < 10000) aNextStopColor.TTColor.addTransformation({ model::TransformationType::Alpha, nAlpha }); sal_Int32 nPosition = static_cast(std::lround((*itC).getStopOffset() * 100000.0)); aColorMap.insert(std::pair{ nPosition, aNextStopColor }); } // If a gradient has only two stops, MS Office renders it with a non-linear method which looks // different than gradient in LibreOffice (see tdf#128795). For more than two stops rendering is // the same as in LibreOffice, even if two stops are identical. if (aColorMap.size() == 2) { auto it = aColorMap.begin(); aColorMap.insert(std::pair{ 0, (*it).second }); } return aColorMap; } } // end namespace void FontworkHelpers::createCharInteropGrabBagUpdatesFromShapeProps( const uno::Reference& rXPropSet, std::vector& rUpdatePropVec) { auto xPropSetInfo = rXPropSet->getPropertySetInfo(); if (!xPropSetInfo.is()) return; // GrabBagStack is a special tool for handling the hierarchy in a GrabBag std::unique_ptr pGrabBagStack; // CharTextFillTextEffect pGrabBagStack.reset(new oox::GrabBagStack("textFill")); drawing::FillStyle eFillStyle = drawing::FillStyle_SOLID; if (xPropSetInfo->hasPropertyByName(u"FillStyle"_ustr)) rXPropSet->getPropertyValue(u"FillStyle"_ustr) >>= eFillStyle; // We might have a solid fill but a transparency gradient. That needs to be exported as gradFill // too, because Word has transparency not separated but in the color stops in a color gradient. // A gradient exists, if the GradientName is not empty. OUString sTransparenceGradientName; if (eFillStyle == drawing::FillStyle_SOLID && xPropSetInfo->hasPropertyByName(u"FillTransparenceGradientName"_ustr) && (rXPropSet->getPropertyValue(u"FillTransparenceGradientName"_ustr) >>= sTransparenceGradientName) && !sTransparenceGradientName.isEmpty()) eFillStyle = drawing::FillStyle_GRADIENT; switch (eFillStyle) { case drawing::FillStyle_NONE: { pGrabBagStack->appendElement("noFill", uno::Any()); break; } case drawing::FillStyle_GRADIENT: { awt::Gradient2 aColorGradient; bool bHasColorGradient(false); awt::Gradient2 aTransparenceGradient; bool bHasTransparenceGradient(false); lcl_getGradientsFromShape(rXPropSet, xPropSetInfo, aColorGradient, bHasColorGradient, aTransparenceGradient, bHasTransparenceGradient); // aColorMap contains the color stops suitable to generate gsLst ColorMapType aColorMap = lcl_createColorMapFromShapeProps( rXPropSet, xPropSetInfo, aColorGradient, bHasColorGradient, aTransparenceGradient, bHasTransparenceGradient); pGrabBagStack->push("gradFill"); pGrabBagStack->push("gsLst"); for (auto it = aColorMap.begin(); it != aColorMap.end(); ++it) { pGrabBagStack->push("gs"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("pos", (*it).first); pGrabBagStack->pop(); if ((*it).second.TTColor.getThemeColorType() == model::ThemeColorType::Unknown) { pGrabBagStack->push("srgbClr"); pGrabBagStack->push("attributes"); pGrabBagStack->addString("val", (*it).second.RGBColor.AsRGBHexString()); pGrabBagStack->pop(); // maCurrentElement:'srgbClr', maPropertyList:'attributes' } else { pGrabBagStack->push("schemeClr"); pGrabBagStack->push("attributes"); pGrabBagStack->addString( "val", lcl_getW14MarkupStringForThemeColor((*it).second.TTColor)); pGrabBagStack->pop(); // maCurrentElement:'schemeClr', maPropertyList:'attributes' } lcl_addColorTransformationToGrabBagStack((*it).second.TTColor, pGrabBagStack); pGrabBagStack->pop(); // maCurrentElement:'gs', maPropertyList:'attributes', 'srgbClr' or 'schemeClr' pGrabBagStack->pop(); // maCurrentElement:'gsLst', maPropertyList: at least two 'gs' } pGrabBagStack->pop(); // maCurrentElement:'gradFill', maPropertyList: gsLst // Kind of gradient awt::GradientStyle eGradientStyle = awt::GradientStyle_LINEAR; if (bHasColorGradient) eGradientStyle = aColorGradient.Style; else if (bHasTransparenceGradient) eGradientStyle = aTransparenceGradient.Style; // write 'lin' or 'path'. LibreOffice has nothing which corresponds to 'shape'. if (eGradientStyle == awt::GradientStyle_LINEAR || eGradientStyle == awt::GradientStyle_AXIAL) { // API angle is in 1/10th deg and describes counter-clockwise rotation of line of // equal color. OOX angle is in 1/60000th deg and describes clockwise rotation of // color transition direction. sal_Int32 nAngleOOX = 0; if (bHasColorGradient) nAngleOOX = ((3600 - aColorGradient.Angle + 900) % 3600) * 6000; else if (bHasTransparenceGradient) nAngleOOX = ((3600 - aTransparenceGradient.Angle + 900) % 3600) * 6000; pGrabBagStack->push("lin"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("ang", nAngleOOX); // LibreOffice cannot scale a gradient to the shape size. pGrabBagStack->addString("scaled", "0"); } else { // Same rendering as in LibreOffice is not possible: // (1) The gradient type 'path' in Word has no rotation. // (2) To get the same size of gradient area, the element 'tileRect' is needed, but // that is not available for element. // So we can only set a reasonably suitable focus point. pGrabBagStack->push("path"); pGrabBagStack->push("attributes"); if (eGradientStyle == awt::GradientStyle_RADIAL || eGradientStyle == awt::GradientStyle_ELLIPTICAL) pGrabBagStack->addString("path", "circle"); else pGrabBagStack->addString("path", "rect"); pGrabBagStack->pop(); pGrabBagStack->push("fillToRect"); pGrabBagStack->push("attributes"); sal_Int32 nLeftPercent = bHasColorGradient ? aColorGradient.XOffset : aTransparenceGradient.XOffset; sal_Int32 nTopPercent = bHasColorGradient ? aColorGradient.YOffset : aTransparenceGradient.YOffset; pGrabBagStack->addInt32("l", nLeftPercent * 1000); pGrabBagStack->addInt32("t", nTopPercent * 1000); pGrabBagStack->addInt32("r", (100 - nLeftPercent) * 1000); pGrabBagStack->addInt32("b", (100 - nTopPercent) * 1000); } // all remaining pop() calls are in the final getRootProperty() method break; } case drawing::FillStyle_SOLID: { pGrabBagStack->push("solidFill"); model::ComplexColor aComplexColor; // It is either "schemeClr" or "srgbClr". if (FontworkHelpers::getThemeColorFromShape("FillComplexColor", rXPropSet, aComplexColor)) { pGrabBagStack->push("schemeClr"); pGrabBagStack->push("attributes"); pGrabBagStack->addString("val", lcl_getW14MarkupStringForThemeColor(aComplexColor)); pGrabBagStack->pop(); // maCurrentElement:'schemeClr', maPropertyList:'attributes' lcl_addColorTransformationToGrabBagStack(aComplexColor, pGrabBagStack); // maCurrentElement:'schemeClr', maPropertyList:'attributes', maybe 'lumMod' and // maybe 'lumOff' } else { pGrabBagStack->push("srgbClr"); sal_Int32 nFillColor(0); if (xPropSetInfo->hasPropertyByName(u"FillColor"_ustr)) rXPropSet->getPropertyValue(u"FillColor"_ustr) >>= nFillColor; pGrabBagStack->push("attributes"); ::Color aColor(ColorTransparency, nFillColor); pGrabBagStack->addString("val", aColor.AsRGBHexString()); pGrabBagStack->pop(); // maCurrentElement:'srgbClr', maPropertyList:'attributes' } sal_Int16 nFillTransparence(0); if (xPropSetInfo->hasPropertyByName(u"FillTransparence"_ustr)) rXPropSet->getPropertyValue(u"FillTransparence"_ustr) >>= nFillTransparence; if (nFillTransparence != 0) { pGrabBagStack->push("alpha"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("val", nFillTransparence * 1000); } // all remaining pop() calls are in the final getRootProperty() method break; } default: // BITMAP is VML only export and does not arrive here. HATCH has to be VML only // export too, but is not yet implemented. break; } // resolve the stack and put resulting PropertyValue into the update vector beans::PropertyValue aCharTextFillTextEffect; aCharTextFillTextEffect.Name = "CharTextFillTextEffect"; aCharTextFillTextEffect.Value <<= pGrabBagStack->getRootProperty(); rUpdatePropVec.push_back(aCharTextFillTextEffect); // CharTextOutlineTextEffect pGrabBagStack.reset(new oox::GrabBagStack("textOutline")); // attributes pGrabBagStack->push("attributes"); // line width sal_Int32 nLineWidth(0); if (xPropSetInfo->hasPropertyByName(u"LineWidth"_ustr)) rXPropSet->getPropertyValue(u"LineWidth"_ustr) >>= nLineWidth; pGrabBagStack->addInt32("w", nLineWidth * 360); // cap for dashes drawing::LineCap eLineCap = drawing::LineCap_BUTT; if (xPropSetInfo->hasPropertyByName(u"LineCap"_ustr)) rXPropSet->getPropertyValue(u"LineCap"_ustr) >>= eLineCap; OUString sCap = u"flat"_ustr; if (eLineCap == drawing::LineCap_ROUND) sCap = u"rnd"_ustr; else if (eLineCap == drawing::LineCap_SQUARE) sCap = u"sq"_ustr; pGrabBagStack->addString("cap", sCap); // LO has no compound lines and always centers the lines pGrabBagStack->addString("cmpd", u"sng"_ustr); pGrabBagStack->addString("alng", u"ctr"_ustr); pGrabBagStack->pop(); // maCurrentElement:'textOutline', maPropertyList:'attributes' // style drawing::LineStyle eLineStyle = drawing::LineStyle_NONE; if (xPropSetInfo->hasPropertyByName(u"LineStyle"_ustr)) rXPropSet->getPropertyValue(u"LineStyle"_ustr) >>= eLineStyle; // 'dashed' is not a separate style in Word. Word has a style 'gradFill', but that is not yet // implemented in LO. So only 'noFill' and 'solidFill'. if (eLineStyle == drawing::LineStyle_NONE) { pGrabBagStack->appendElement("noFill", uno::Any()); } else { pGrabBagStack->push("solidFill"); // It is either "schemeClr" or "srgbClr". model::ComplexColor aComplexColor; if (FontworkHelpers::getThemeColorFromShape("LineComplexColor", rXPropSet, aComplexColor)) { pGrabBagStack->push("schemeClr"); pGrabBagStack->push("attributes"); pGrabBagStack->addString("val", lcl_getW14MarkupStringForThemeColor(aComplexColor)); pGrabBagStack->pop(); lcl_addColorTransformationToGrabBagStack(aComplexColor, pGrabBagStack); // maCurrentElement:'schemeClr', maPropertylist:'attributes' } else // not a theme color { pGrabBagStack->push("srgbClr"); pGrabBagStack->push("attributes"); sal_Int32 nLineColor(0); if (xPropSetInfo->hasPropertyByName(u"LineColor"_ustr)) rXPropSet->getPropertyValue(u"LineColor"_ustr) >>= nLineColor; ::Color aColor(ColorTransparency, nLineColor); pGrabBagStack->addString("val", aColor.AsRGBHexString()); pGrabBagStack->pop(); // maCurrentElement:'srgbClr', maPropertylist:'attributes' } sal_Int16 nLineTransparence(0); if (xPropSetInfo->hasPropertyByName(u"LineTransparence"_ustr)) rXPropSet->getPropertyValue(u"LineTransparence"_ustr) >>= nLineTransparence; if (nLineTransparence != 0) { pGrabBagStack->push("alpha"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("val", nLineTransparence * 1000); pGrabBagStack->pop(); // maCurrentElement: 'alpha' pGrabBagStack->pop(); // maCurrentElement: 'srgbClr' or 'schemeClr' } pGrabBagStack->pop(); // maCurrentElement:'solidFill', maPropertyList:either 'srgbClr' or 'schemeClr pGrabBagStack->pop(); } // maCurrentElement:'textOutline', maPropertyList:'attributes' and either 'noFill' or 'solidFill' // prstDash if (eLineStyle == drawing::LineStyle_DASH) { pGrabBagStack->push("prstDash"); OUString sPrstDash = u"sysDot"_ustr; drawing::LineDash aLineDash; if (xPropSetInfo->hasPropertyByName(u"LineDash"_ustr) && (rXPropSet->getPropertyValue(u"LineDash"_ustr) >>= aLineDash)) { // The outline of abc-transform in Word is not able to use custDash. But we know the line // is dashed. We keep "sysDot" as fallback in case no prstDash is detected. FontworkHelpers::createPrstDashFromLineDash(aLineDash, eLineCap, sPrstDash); } else { // ToDo: There may be a named dash style, but that is unlikely for Fontwork shapes. So // I skip it for now and use the "sysDot" fallback. } pGrabBagStack->push("attributes"); pGrabBagStack->addString("val", sPrstDash); pGrabBagStack->pop(); // maCurrentElement:'prstDash' pGrabBagStack->pop(); // maCurrentElement:'textOutline' } // maCurrentElement:'textOutline', maPropertyList:'attributes', either 'noFill' or 'solidFill', // and maybe 'prstDash'. // LineJoint, can be 'round', 'bevel' or 'miter' in Word drawing::LineJoint eLineJoint = drawing::LineJoint_NONE; if (xPropSetInfo->hasPropertyByName(u"LineJoint"_ustr)) rXPropSet->getPropertyValue(u"LineJoint"_ustr) >>= eLineJoint; if (eLineJoint == drawing::LineJoint_NONE || eLineJoint == drawing::LineJoint_BEVEL) pGrabBagStack->appendElement("bevel", uno::Any()); else if (eLineJoint == drawing::LineJoint_ROUND) pGrabBagStack->appendElement("round", uno::Any()); else // MITER or deprecated MIDDLE { pGrabBagStack->push("miter"); pGrabBagStack->push("attributes"); pGrabBagStack->addInt32("lim", 0); // As of Feb. 2023 LO cannot render other values. pGrabBagStack->pop(); // maCurrentElement:'attributes' pGrabBagStack->pop(); // maCurrentElement:'miter' } // maCurrentElement:'textOutline', maPropertyList:'attributes', either 'noFill' or // 'solidFill', maybe 'prstDash', and either 'bevel', 'round' or 'miter'. // resolve the stack and put resulting PropertyValue into the update vector beans::PropertyValue aCharTextOutlineTextEffect; aCharTextOutlineTextEffect.Name = "CharTextOutlineTextEffect"; aCharTextOutlineTextEffect.Value <<= pGrabBagStack->getRootProperty(); rUpdatePropVec.push_back(aCharTextOutlineTextEffect); // CharThemeOriginalColor, CharThemeColor, and CharThemeColorShade or CharThemeColorTint will be // used for element. That is evaluated by applications, which do not understand w14 // namespace, or if w14:textFill is omitted. model::ComplexColor aComplexColor; if (FontworkHelpers::getThemeColorFromShape("FillComplexColor", rXPropSet, aComplexColor)) { // CharThemeColor beans::PropertyValue aCharThemeColor; aCharThemeColor.Name = u"CharThemeColor"_ustr; aCharThemeColor.Value <<= lcl_getWMarkupStringForThemeColor(aComplexColor); rUpdatePropVec.push_back(aCharThemeColor); // CharThemeColorShade or CharThemeColorTint // MS Office uses themeTint and themeShade on the luminance in a HSL color space, see 2.1.72 // in [MS-OI29500]. That is different from OOXML specification. // We made two assumption here: (1) If LumOff exists and is not zero, it is a 'tint'. // (2) LumMod + LumOff == 10000; sal_Int16 nLumMod; if (lcl_getThemeColorTransformationValue(aComplexColor, model::TransformationType::LumMod, nLumMod)) { sal_Int16 nLumOff; bool bIsTint = lcl_getThemeColorTransformationValue( aComplexColor, model::TransformationType::LumOff, nLumOff) && nLumOff != 0; sal_uInt8 nValue = std::clamp(lround(double(nLumMod) * 255.0 / 10000.0), 0, 255); OUString sValue = OUString::number(nValue, 16); beans::PropertyValue aCharThemeTintOrShade; aCharThemeTintOrShade.Name = bIsTint ? u"CharThemeColorTint" : u"CharThemeColorShade"; aCharThemeTintOrShade.Value <<= sValue; rUpdatePropVec.push_back(aCharThemeTintOrShade); } } // ToDo: Are FillColorLumMod, FillColorLumOff and FillColorTheme possible without // FillComplexColor? If yes, we need an 'else' part here. // CharThemeOriginalColor. beans::PropertyValue aCharThemeOriginalColor; sal_Int32 nFillColor(0); if (xPropSetInfo->hasPropertyByName(u"FillColor"_ustr)) rXPropSet->getPropertyValue(u"FillColor"_ustr) >>= nFillColor; aCharThemeOriginalColor.Name = u"CharThemeOriginalColor"_ustr; ::Color aColor(ColorTransparency, nFillColor); aCharThemeOriginalColor.Value <<= aColor.AsRGBHEXString(); rUpdatePropVec.push_back(aCharThemeOriginalColor); } void FontworkHelpers::applyUpdatesToCharInteropGrabBag( const std::vector& rUpdatePropVec, uno::Reference& rXText) { if (!rXText.is()) return; uno::Reference rXTextCursor = rXText->createTextCursor(); rXTextCursor->gotoStart(false); rXTextCursor->gotoEnd(true); uno::Reference paraEnumAccess(rXText, 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; // Now apply the updates to the CharInteropGrabBag of this run uno::Sequence aCharInteropGrabBagSeq; if (xRunPropSetInfo->hasPropertyByName("CharInteropGrabBag")) xRunPropSet->getPropertyValue("CharInteropGrabBag") >>= aCharInteropGrabBagSeq; comphelper::SequenceAsHashMap aGrabBagMap(aCharInteropGrabBagSeq); for (const auto& rProp : rUpdatePropVec) { aGrabBagMap[rProp.Name] = rProp.Value; // [] inserts if not exists } xRunPropSet->setPropertyValue("CharInteropGrabBag", uno::Any(aGrabBagMap.getAsConstPropertyValueList())); } } } /* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */