tdf#51195 add docx export of gradient fill of Fontwork shapes
FillGradient, which is a awt::Gradient, has many features which cannot
be represented in the <w14:textFill> element in docx. Therefore often
only workarounds are possible.
ELLIPTICAL and RADIAL are exported to 'circle', SQUARE and RECT to
'rect'. 'Angle' is ignored. A focus point is used instead of a focus
line.
LINEAR and AXIAL are exported to 'lin'. AXIAL is done be compress and
mirroring the color stops. Using Words feature of reflecting a gradient
would prevent detecting 'axial' in the current import filter.
'Border' is exported by introducing additional color stops.
'StepCount' is ignored. A workaround using additional color stops is
possible, but would require a simultaneous change of the import filter.
'StartIntensity' and 'EndIntensity' are exported as 'lumMod'.
Theme colors are considered where they can currently occur. But
tdf#151882 is yet not fixed, so Word will not render them because of
missing Theme folder.
To allow 'lumMod' and theme color and RGB color as well, the color of
a color stop is hold in a struct.
In case of two color stops, the color stop at position 0% is doubled.
That way Word uses the same linear color transition as LO and not its
quadratic one. AXIAL too introduces two color stops at position 50%.
Emulating 'StepCount' would produce two color stops at same position
too. Therefore a std::multimap is used for the color stops.
The implementation has a lot local parts. If they should be useful
for Fontwork shapes in Impress/Draw, they can be moved and adapted
later. The implementation separates the calculation of the required
color stops from the generation of the markup, so using parts in
Impress/Draw is likely possible.
Change-Id: I1032ab8d37b6f112d66f85a30210ebda3ae54486
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/148354
Tested-by: Jenkins
Reviewed-by: Miklos Vajna <vmiklos@collabora.com>
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt
new file mode 100644
index 0000000..99fe4d8a
--- /dev/null
+++ b/oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt
Binary files differ
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt
new file mode 100644
index 0000000..c036e13
--- /dev/null
+++ b/oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt
Binary files differ
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt
new file mode 100644
index 0000000..bc821db
--- /dev/null
+++ b/oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt
Binary files differ
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt
new file mode 100644
index 0000000..746b60b
--- /dev/null
+++ b/oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt
Binary files differ
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt
new file mode 100644
index 0000000..d4daee9
--- /dev/null
+++ b/oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt
Binary files differ
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt
new file mode 100644
index 0000000..86e71ba
--- /dev/null
+++ b/oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt
Binary files differ
diff --git a/oox/qa/unit/export.cxx b/oox/qa/unit/export.cxx
index 645ffe3..5a90126 100644
--- a/oox/qa/unit/export.cxx
+++ b/oox/qa/unit/export.cxx
@@ -1015,6 +1015,288 @@ CPPUNIT_TEST_FIXTURE(Test, testFontworkDistance)
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:bodyPr",
{ { "lIns", "0" }, { "rIns", "0" }, { "tIns", "0" }, { "bIns", "0" } });
}
CPPUNIT_TEST_FIXTURE(Test, testFontworkLinGradientRGBColor)
{
// The document has a Fontwork shape with UI settings: linear gradient fill with angle 330deg,
// start color #ffff00 (Yellow) with 'Brightness' 80%, end color #4682B4 (Steel Blue), Transition
// Start 25% and solid transparency 30%.
// Without fix the gradient was not exported at all.
loadFromURL(u"tdf51195_Fontwork_linearGradient.odt");
// FIXME: tdf#153183 validation error in OOXML export: Errors: 1
// Attribute 'ID' is not allowed to appear in element 'v:shape'.
skipValidation();
// Save to DOCX:
save("Office Open XML Text");
// Examine the saved markup.
xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
// path to shape text run properties
OString sElement = "/w:document/w:body/w:p/w:r/mc:AlternateContent/mc:Choice/w:drawing/"
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:txbx/w:txbxContent/w:p/w:r/"
"w:rPr/";
// Make sure w14:textFill and w14:gradFill elements exist with child elements
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst", 1);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst/w14:gs", 3);
// 330deg gradient rotation = 120deg color transition direction
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:lin", "ang", "7200000");
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:lin", "scaled", "0");
// Make sure the color stops have correct position and color
sElement += "w14:textFill/w14:gradFill/w14:gsLst/";
assertXPath(pXmlDoc, sElement + "w14:gs[1]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr", "val", "ffff00");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr/w14:lumMod", "val", "80000");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr/w14:alpha", "val", "30000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]", "pos", "25000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr", "val", "ffff00");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr/w14:lumMod", "val", "80000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr/w14:alpha", "val", "30000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]", "pos", "100000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr", "val", "4682b4");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr/w14:alpha", "val", "30000");
}
CPPUNIT_TEST_FIXTURE(Test, testFontworkAxialGradientTransparency)
{
// The document has a Fontwork shape with UI settings: solid fill theme color Accen3 25% darker,
// Transparency gradient Type Axial with Angle 160deg, Transition start 40%,
// Start value 5%, End value 90%
// Without fix the gradient was not exported at all.
loadFromURL(u"tdf51195_Fontwork_axialGradient.odt");
// FIXME: tdf#153183 validation error in OOXML export: Errors: 1
// Attribute 'ID' is not allowed to appear in element 'v:shape'.
skipValidation();
// Save to DOCX:
save("Office Open XML Text");
// Examine the saved markup.
xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
// path to shape text run properties
OString sElement = "/w:document/w:body/w:p/w:r/mc:AlternateContent/mc:Choice/w:drawing/"
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:txbx/w:txbxContent/w:p/w:r/"
"w:rPr/";
// Make sure w14:textFill and w14:gradFill elements exist with child elements
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst", 1);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst/w14:gs", 6);
// 160deg gradient rotation = 290deg (360deg-160deg+90deg) color transition direction
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:lin", "ang", "17400000");
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:lin", "scaled", "0");
// Make sure the color stops have correct position and color
sElement += "w14:textFill/w14:gradFill/w14:gsLst/";
// gradient is in transparency, color is always the same.
for (char ch = '1'; ch <= '6'; ++ch)
{
assertXPath(pXmlDoc, sElement + "w14:gs[" + OStringChar(ch) + "]/w14:schemeClr", "val",
"accent3");
assertXPath(pXmlDoc, sElement + "w14:gs[" + OStringChar(ch) + "]/w14:schemeClr/w14:lumMod",
"val", "75000");
}
// outer transparency
assertXPath(pXmlDoc, sElement + "w14:gs[1]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:schemeClr/w14:alpha", "val", "90000");
// border, same transparency
assertXPath(pXmlDoc, sElement + "w14:gs[2]", "pos", "20000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:schemeClr/w14:alpha", "val", "90000");
// gradient to inner transparency at center
assertXPath(pXmlDoc, sElement + "w14:gs[3]", "pos", "50000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:schemeClr/w14:alpha", "val", "5000");
// from inner transparency at center
assertXPath(pXmlDoc, sElement + "w14:gs[4]", "pos", "50000");
assertXPath(pXmlDoc, sElement + "w14:gs[4]/w14:schemeClr/w14:alpha", "val", "5000");
// mirrored gradient to outer transparency
assertXPath(pXmlDoc, sElement + "w14:gs[5]", "pos", "80000");
assertXPath(pXmlDoc, sElement + "w14:gs[5]/w14:schemeClr/w14:alpha", "val", "90000");
// mirrored border
assertXPath(pXmlDoc, sElement + "w14:gs[6]", "pos", "100000");
assertXPath(pXmlDoc, sElement + "w14:gs[6]/w14:schemeClr/w14:alpha", "val", "90000");
}
CPPUNIT_TEST_FIXTURE(Test, testFontworkRadialGradient)
{
// The document has a Fontwork shape with UI settings: gradient fill, Type radial,
// From Color #40E0D0, To Color #FF0000, Center x|y 75%|20%, no transparency
// Transition start 10%
// Without fix the gradient was not exported at all.
loadFromURL(u"tdf51195_Fontwork_radialGradient.odt");
// FIXME: tdf#153183 validation error in OOXML export: Errors: 1
// Attribute 'ID' is not allowed to appear in element 'v:shape'.
skipValidation();
// Save to DOCX:
save("Office Open XML Text");
// Examine the saved markup.
xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
// path to shape text run properties
OString sElement = "/w:document/w:body/w:p/w:r/mc:AlternateContent/mc:Choice/w:drawing/"
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:txbx/w:txbxContent/w:p/w:r/"
"w:rPr/";
// Make sure w14:textFill and w14:gradFill elements exist with child elements
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst", 1);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst/w14:gs", 3);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path", "path", "circle");
assertXPathAttrs(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path/w14:fillToRect",
{ { "l", "75000" }, { "t", "20000" }, { "r", "25000" }, { "b", "80000" } });
// Make sure the color stops have correct position and color
sElement += "w14:textFill/w14:gradFill/w14:gsLst/";
assertXPath(pXmlDoc, sElement + "w14:gs[1]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr", "val", "ff0000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]", "pos", "90000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr", "val", "40e0d0");
assertXPath(pXmlDoc, sElement + "w14:gs[3]", "pos", "100000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr", "val", "40e0d0");
}
CPPUNIT_TEST_FIXTURE(Test, testFontworkEllipticalGradient)
{
// The document has a Fontwork shape with UI settings: solid fill, Color #00008B (deep blue),
// transparency gradient type Ellipsoid, Center x|y 50%|50%, Transition Start 50%,
// Start 70%, End 0%.
// Without fix the gradient was not exported at all.
loadFromURL(u"tdf51195_Fontwork_ellipticalGradient.odt");
// FIXME: tdf#153183 validation error in OOXML export: Errors: 1
// Attribute 'ID' is not allowed to appear in element 'v:shape'.
skipValidation();
// Save to DOCX:
save("Office Open XML Text");
// Examine the saved markup.
xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
// path to shape text run properties
OString sElement = "/w:document/w:body/w:p/w:r/mc:AlternateContent/mc:Choice/w:drawing/"
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:txbx/w:txbxContent/w:p/w:r/"
"w:rPr/";
// Make sure w14:textFill and w14:gradFill elements exist with child elements
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst", 1);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst/w14:gs", 3);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path", "path", "circle");
assertXPathAttrs(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path/w14:fillToRect",
{ { "l", "50000" }, { "t", "50000" }, { "r", "50000" }, { "b", "50000" } });
// Make sure the color stops have correct position and color
sElement += "w14:textFill/w14:gradFill/w14:gsLst/";
assertXPath(pXmlDoc, sElement + "w14:gs[1]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr", "val", "00008b");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr/w14:alpha", 0);
assertXPath(pXmlDoc, sElement + "w14:gs[2]", "pos", "50000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr", "val", "00008b");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr/w14:alpha", "val", "70000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]", "pos", "100000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr", "val", "00008b");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr/w14:alpha", "val", "70000");
}
CPPUNIT_TEST_FIXTURE(Test, testFontworkSquareGradient)
{
// The document has a Fontwork shape with UI settings: gradient fill Type "Quadratic" (which is
// "square" in ODF and API), From Color #4963ef 40%, To Color #ffff6e 90%, Center x|y 100%|50%,
// no transparency
// Without fix the gradient was not exported at all.
loadFromURL(u"tdf51195_Fontwork_squareGradient.odt");
// FIXME: tdf#153183 validation error in OOXML export: Errors: 1
// Attribute 'ID' is not allowed to appear in element 'v:shape'.
skipValidation();
// Save to DOCX:
save("Office Open XML Text");
// Examine the saved markup.
xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
// path to shape text run properties
OString sElement = "/w:document/w:body/w:p/w:r/mc:AlternateContent/mc:Choice/w:drawing/"
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:txbx/w:txbxContent/w:p/w:r/"
"w:rPr/";
// Make sure w14:textFill and w14:gradFill elements exist with child elements
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst", 1);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst/w14:gs", 3);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path", "path", "rect");
assertXPathAttrs(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path/w14:fillToRect",
{ { "l", "100000" }, { "t", "50000" }, { "r", "0" }, { "b", "50000" } });
// Make sure the color stops have correct position and color
sElement += "w14:textFill/w14:gradFill/w14:gsLst/";
assertXPath(pXmlDoc, sElement + "w14:gs[1]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr", "val", "ffff6e");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:srgbClr/w14:lumMod", "val", "90000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr", "val", "ffff6e");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:srgbClr/w14:lumMod", "val", "90000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]", "pos", "100000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr", "val", "49b3ef");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:srgbClr/w14:lumMod", "val", "40000");
}
CPPUNIT_TEST_FIXTURE(Test, testFontworkRectGradient)
{
// The document has a Fontwork shape with UI settings: solid color theme Accent 4 60% lighter,
// transparency gradient Type "Square" (which is "rectangle" in ODF and API, tdf#154071),
// Center x|y 50%|50%, Transition start 10%, Start value 70%, End value 5%.
// Without fix the gradient was not exported at all.
loadFromURL(u"tdf51195_Fontwork_rectGradient.odt");
// FIXME: tdf#153183 validation error in OOXML export: Errors: 1
// Attribute 'ID' is not allowed to appear in element 'v:shape'.
skipValidation();
// Save to DOCX:
save("Office Open XML Text");
// Examine the saved markup.
xmlDocUniquePtr pXmlDoc = parseExport("word/document.xml");
// path to shape text run properties
OString sElement = "/w:document/w:body/w:p/w:r/mc:AlternateContent/mc:Choice/w:drawing/"
"wp:anchor/a:graphic/a:graphicData/wps:wsp/wps:txbx/w:txbxContent/w:p/w:r/"
"w:rPr/";
// Make sure w14:textFill and w14:gradFill elements exist with child elements
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst", 1);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:gsLst/w14:gs", 3);
assertXPath(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path", "path", "rect");
assertXPathAttrs(pXmlDoc, sElement + "w14:textFill/w14:gradFill/w14:path/w14:fillToRect",
{ { "l", "50000" }, { "t", "50000" }, { "r", "50000" }, { "b", "50000" } });
// Make sure the color stops have correct position and color
sElement += "w14:textFill/w14:gradFill/w14:gsLst/";
assertXPath(pXmlDoc, sElement + "w14:gs[1]", "pos", "0");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:schemeClr", "val", "accent4");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:schemeClr/w14:lumMod", "val", "40000");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:schemeClr/w14:lumOff", "val", "60000");
assertXPath(pXmlDoc, sElement + "w14:gs[1]/w14:schemeClr/w14:alpha", "val", "5000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]", "pos", "90000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:schemeClr", "val", "accent4");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:schemeClr/w14:lumMod", "val", "40000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:schemeClr/w14:lumOff", "val", "60000");
assertXPath(pXmlDoc, sElement + "w14:gs[2]/w14:schemeClr/w14:alpha", "val", "70000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]", "pos", "100000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:schemeClr", "val", "accent4");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:schemeClr/w14:lumMod", "val", "40000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:schemeClr/w14:lumOff", "val", "60000");
assertXPath(pXmlDoc, sElement + "w14:gs[3]/w14:schemeClr/w14:alpha", "val", "70000");
}
}
CPPUNIT_PLUGIN_IMPLEMENT();
diff --git a/oox/source/drawingml/fontworkhelpers.cxx b/oox/source/drawingml/fontworkhelpers.cxx
index c339eb2..cebe7bc 100644
--- a/oox/source/drawingml/fontworkhelpers.cxx
+++ b/oox/source/drawingml/fontworkhelpers.cxx
@@ -26,11 +26,14 @@
#include <docmodel/uno/UnoThemeColor.hxx>
#include <drawingml/customshapeproperties.hxx>
#include <drawingml/presetgeometrynames.hxx>
#include <oox/drawingml/drawingmltypes.hxx>
#include <oox/helper/grabbagstack.hxx>
#include <sal/log.hxx>
#include <svx/msdffdef.hxx>
#include <tools/color.hxx>
#include <tools/helpers.hxx>
#include <com/sun/star/awt/Gradient.hpp>
#include <com/sun/star/beans/PropertyAttribute.hpp>
#include <com/sun/star/beans/PropertyValue.hpp>
#include <com/sun/star/beans/XPropertySet.hpp>
@@ -1021,6 +1024,23 @@ bool FontworkHelpers::getThemeColorFromShape(
namespace
{
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::ThemeColor 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 <w14:gs> element in oox, 'second' contains color and color transformations
// at this position. The map contains all information needed for a <w14:gsLst> element in oox.
typedef std::multimap<sal_Int32, gradientStopColor> ColorMapType;
namespace
{
// Returns the string to be used in w14:schemeClr in case of w14:textOutline or w14:textFill
OUString lcl_getW14MarkupStringForThemeColor(const model::ThemeColor& rThemeColor)
{
@@ -1061,9 +1081,9 @@ bool lcl_getThemeColorTransformationValue(const model::ThemeColor& rThemeColor,
return true;
}
// Adds the child elements 'lumMod' and/or 'lumOff' to 'schemeClr' maCurrentElement of
// pGrabStack, if such exist in rThemeColor. As of Feb 2023, 'alpha' is not contained in the
// the maTransformations of rThemeColor.
// Adds the child elements 'lumMod' and 'lumOff' to 'schemeClr' maCurrentElement of pGrabStack,
// if such exist in rThemeColor. 'alpha' is contained in the maTransformations of rThemeColor
// in case of gradient fill.
void lcl_addColorTransformationToGrabBagStack(const model::ThemeColor& rThemeColor,
std::unique_ptr<oox::GrabBagStack>& pGrabBagStack)
{
@@ -1087,11 +1107,303 @@ void lcl_addColorTransformationToGrabBagStack(const model::ThemeColor& rThemeCol
pGrabBagStack->pop();
pGrabBagStack->pop();
break;
default: // other child element can be added later if needed for Fontwork
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<beans::XPropertySet>& rXPropSet,
const uno::Reference<beans::XPropertySetInfo>& rXPropSetInfo,
awt::Gradient& rColorGradient, bool& rbHasColorGradient,
awt::Gradient& rTransparenceGradient,
bool& rbHasTransparenceGradient)
{
OUString sColorGradientName;
rbHasColorGradient
= rXPropSetInfo->hasPropertyByName(u"FillGradientName")
&& (rXPropSet->getPropertyValue(u"FillGradientName") >>= sColorGradientName)
&& !sColorGradientName.isEmpty() && rXPropSetInfo->hasPropertyByName(u"FillGradient")
&& (rXPropSet->getPropertyValue(u"FillGradient") >>= rColorGradient);
OUString sTransparenceGradientName;
rbHasTransparenceGradient
= rXPropSetInfo->hasPropertyByName(u"FillTransparenceGradientName")
&& (rXPropSet->getPropertyValue(u"FillTransparenceGradientName")
>>= sTransparenceGradientName)
&& !sTransparenceGradientName.isEmpty()
&& rXPropSetInfo->hasPropertyByName(u"FillTransparenceGradient")
&& (rXPropSet->getPropertyValue(u"FillTransparenceGradient") >>= rTransparenceGradient);
}
// Returns color without transparency and without intensity. rnPos is position in gradient
// definition from 0 (= 0%) to 100 (=100%), without considering the gradient type. The border is at
// 0% side. The caller takes care to use a suitable position and gradient.
::Color lcl_getColorFromColorGradient(const awt::Gradient& rColorGradient, const sal_Int32 rnPos)
{
sal_Int16 nBorder = rColorGradient.Border; // Border is in percent
::Color aStartColor(ColorTransparency, rColorGradient.StartColor);
if (rnPos <= 0 || rnPos <= nBorder || nBorder >= 100)
return aStartColor;
::Color aEndColor(ColorTransparency, rColorGradient.EndColor);
if (rnPos >= 100)
return aEndColor;
// linear interpolation for nBorder < rnpos < 100 in each color component
auto ColorInterpolate = [rnPos, nBorder](sal_uInt8 nStartC, sal_uInt8 nEndC) -> sal_uInt8 {
return std::clamp<sal_uInt8>(
std::lround((nStartC * (100 - rnPos) + nEndC * (rnPos - nBorder)) / (100.0 - nBorder)),
0, 255);
};
sal_uInt8 nInterpolatedRed = ColorInterpolate(aStartColor.GetRed(), aEndColor.GetRed());
sal_uInt8 nInterpolatedGreen = ColorInterpolate(aStartColor.GetGreen(), aEndColor.GetGreen());
sal_uInt8 nInterpolatedBlue = ColorInterpolate(aStartColor.GetBlue(), aEndColor.GetBlue());
return ::Color(nInterpolatedRed, nInterpolatedGreen, nInterpolatedBlue);
}
// returns intensity in percent. rnPos is position in gradient definition from
// 0 (= 0%) to 100 (=100%), without considering the gradient type. The border is at 0% side.
// The caller takes care to use a suitable position and gradient.
sal_Int16 lcl_getIntensityFromColorGradient(const awt::Gradient& rColorGradient,
const sal_Int32 rnPos)
{
sal_Int16 nBorder = rColorGradient.Border; // Border is in percent
sal_Int16 nStartIntensity = rColorGradient.StartIntensity;
if (rnPos <= 0 || rnPos <= nBorder || nBorder >= 100)
return nStartIntensity;
sal_Int32 nEndIntensity = rColorGradient.EndIntensity;
if (rnPos >= 100)
return nEndIntensity;
// linear interpolation for nBorder < npos < 100
return std::lround((nStartIntensity * (100 - rnPos) + nEndIntensity * (rnPos - nBorder))
/ (100.0 - nBorder));
}
// returns transparency in percent. rnPos is position in gradient definition from
// 0 (= 0%) to 100 (=100%), without considering the gradient type. The border is at 0% side.
// The caller takes care to use a suitable position and gradient.
sal_Int16 lcl_getAlphaFromTransparenceGradient(const awt::Gradient& rTransparenceGradient,
const sal_Int32 rnPos)
{
sal_Int16 nBorder = rTransparenceGradient.Border; // Border is in percent
// The transparency is not in Start- or EndIntensity, but encoded into the Color as gray.
::Color aStartColor(ColorTransparency, rTransparenceGradient.StartColor);
if (rnPos <= 0 || rnPos <= nBorder || nBorder >= 100)
return std::lround(aStartColor.GetRed() * 100 / 255.0);
::Color aEndColor(ColorTransparency, rTransparenceGradient.EndColor);
if (rnPos >= 100)
return std::lround(aEndColor.GetRed() * 100 / 255.0);
// linear interpolation for nBorder < npos < 100
return std::lround(
(aStartColor.GetRed() * (100 - rnPos) + aEndColor.GetRed() * (rnPos - nBorder))
/ (100.0 - nBorder) * 100 / 255.0);
}
// gradientStopColor has components ::Color RGBColor and modul::ThemeColor TTColor
gradientStopColor
lcl_createGradientStopColor(const uno::Reference<beans::XPropertySet>& rXPropSet,
const uno::Reference<beans::XPropertySetInfo>& rXPropSetInfo,
const awt::Gradient& rColorGradient, const bool& rbHasColorGradient,
const awt::Gradient& rTransparenceGradient,
const bool& rbHasTransparenceGradient, const sal_Int32& rnPos)
{
// Component mnValue of Tranformation struct is in 1/100th percent (e.g 80% = 8000) in range
// -10000 to +10000. Constants are used in converting from API values below.
constexpr sal_Int16 nFactorToHthPerc = 100;
constexpr sal_Int16 nMaxHthPerc = 10000;
gradientStopColor aStopColor;
if (rbHasTransparenceGradient)
{
// Color
if (rbHasColorGradient)
{
// a color gradient is yet not enabled to use theme colors
aStopColor.RGBColor = lcl_getColorFromColorGradient(rColorGradient, rnPos);
aStopColor.TTColor.setType(model::ThemeColorType::Unknown);
sal_Int16 nIntensity = lcl_getIntensityFromColorGradient(rColorGradient, rnPos);
if (nIntensity != 100)
aStopColor.TTColor.addTransformation(
{ model::TransformationType::LumMod,
std::clamp<sal_Int16>(nIntensity * nFactorToHthPerc, -nMaxHthPerc,
nMaxHthPerc) });
}
else // solid color
{
// fill color might be a theme color
if (!(FontworkHelpers::getThemeColorFromShape("FillColorThemeReference", rXPropSet,
aStopColor.TTColor)))
{
// no theme color, use FillColor
sal_Int32 nFillColor(0);
if (rXPropSetInfo->hasPropertyByName("FillColor"))
rXPropSet->getPropertyValue(u"FillColor") >>= nFillColor;
aStopColor.RGBColor = ::Color(ColorTransparency, nFillColor);
aStopColor.TTColor.setType(model::ThemeColorType::Unknown);
}
}
// transparency
// Mixed gradient types for color and transparency are not possible in oox. For now we act as
// if gradient geometries are identical. That is the case if we get the gradient from oox
// import.
sal_Int16 nAlpha = lcl_getAlphaFromTransparenceGradient(rTransparenceGradient, rnPos);
// model::TransformationType::Alpha is designed to be used with a:alpha, which has opacity.
// Therefore convert transparency to opacity.
if (nAlpha > 0)
aStopColor.TTColor.addTransformation(
{ model::TransformationType::Alpha,
std::clamp<sal_Int16>(nMaxHthPerc - nAlpha * nFactorToHthPerc, -nMaxHthPerc,
nMaxHthPerc) });
return aStopColor;
}
// else solid transparency or no transparency
// color
if (rbHasColorGradient)
{
// a color gradient is yet not enabled to use theme colors
aStopColor.RGBColor = lcl_getColorFromColorGradient(rColorGradient, rnPos);
aStopColor.TTColor.setType(model::ThemeColorType::Unknown);
sal_Int16 nIntensity = lcl_getIntensityFromColorGradient(rColorGradient, rnPos);
if (nIntensity != 100)
aStopColor.TTColor.addTransformation(
{ model::TransformationType::LumMod,
std::clamp<sal_Int16>(nIntensity * nFactorToHthPerc, -nMaxHthPerc,
nMaxHthPerc) });
}
else
{
// solid color and solid transparency
SAL_WARN("oox.drawingml", "methode should not be called in this case");
if (!(FontworkHelpers::getThemeColorFromShape("FillColorThemeReference", rXPropSet,
aStopColor.TTColor)))
{
// no theme color, use FillColor
sal_Int32 nFillColor(0);
if (rXPropSetInfo->hasPropertyByName(u"FillColor"))
rXPropSet->getPropertyValue(u"FillColor") >>= nFillColor;
aStopColor.RGBColor = ::Color(ColorTransparency, nFillColor);
aStopColor.TTColor.setType(model::ThemeColorType::Unknown);
}
}
// Maybe transparency from FillTransparence
// model::TransformationType::Alpha is designed to be used with a:alpha, which has opacity.
// Therefore convert transparency to opacity.
sal_Int16 nAlpha(0);
if (rXPropSetInfo->hasPropertyByName(u"FillTransparence")
&& (rXPropSet->getPropertyValue(u"FillTransparence") >>= nAlpha) && nAlpha > 0)
aStopColor.TTColor.addTransformation(
{ model::TransformationType::Alpha,
std::clamp<sal_Int16>(nMaxHthPerc - nAlpha * nFactorToHthPerc, -nMaxHthPerc,
nMaxHthPerc) });
return aStopColor;
}
ColorMapType lcl_createColorMapFromShapeProps(
const uno::Reference<beans::XPropertySet>& rXPropSet,
const uno::Reference<beans::XPropertySetInfo>& rXPropSetInfo,
const awt::Gradient& rColorGradient, const bool& rbHasColorGradient,
const awt::Gradient& rTransparenceGradient, const bool& rbHasTransparenceGradient)
{
ColorMapType aColorMap;
awt::Gradient aColorGradient = rColorGradient;
awt::Gradient aTransparenceGradient = rTransparenceGradient;
// AXIAL has reversed gradient direction. Change it so, that 'border' is at 'start'.
if (rbHasColorGradient && aColorGradient.Style == awt::GradientStyle_AXIAL)
{
std::swap<sal_Int32>(aColorGradient.StartColor, aColorGradient.EndColor);
std::swap<sal_Int16>(aColorGradient.StartIntensity, aColorGradient.EndIntensity);
}
if (rbHasTransparenceGradient && aTransparenceGradient.Style == awt::GradientStyle_AXIAL)
{
std::swap<sal_Int32>(aTransparenceGradient.StartColor, aTransparenceGradient.EndColor);
std::swap<sal_Int16>(aTransparenceGradient.StartIntensity,
aTransparenceGradient.EndIntensity);
}
// A gradientStopColor includes color and transparency.
// The key of aColorMap has same unit as the w14:pos attribute of <w14:gs> element in oox.
gradientStopColor aStartStopColor
= lcl_createGradientStopColor(rXPropSet, rXPropSetInfo, aColorGradient, rbHasColorGradient,
aTransparenceGradient, rbHasTransparenceGradient, 0);
aColorMap.insert(std::pair{ 0, aStartStopColor });
gradientStopColor aEndStopColor
= lcl_createGradientStopColor(rXPropSet, rXPropSetInfo, aColorGradient, rbHasColorGradient,
aTransparenceGradient, rbHasTransparenceGradient, 100);
aColorMap.insert(std::pair{ 100000, aEndStopColor });
// We add additional gradientStopColor in case of borders.
if (rbHasColorGradient)
{
// We only use the color border for now. If the transparency gradient has a total different
// geometry than the color gradient, a description is not possible in oox.
// ToDo: If geometries only differ in border, emulation is possible.
sal_Int32 nBorderPos = aColorGradient.Border * 1000;
if (nBorderPos > 0)
aColorMap.insert(std::pair{ nBorderPos, aStartStopColor });
}
else if (rbHasTransparenceGradient)
{
sal_Int32 nBorderPos = aTransparenceGradient.Border * 1000;
if (nBorderPos > 0)
aColorMap.insert(std::pair{ nBorderPos, aStartStopColor });
}
// In case of AXIAL we compress the gradient to half wide and mirror it to the other half.
if ((rbHasColorGradient && aColorGradient.Style == awt::GradientStyle_AXIAL)
|| (!rbHasColorGradient && rbHasTransparenceGradient
&& aTransparenceGradient.Style == awt::GradientStyle_AXIAL))
{
ColorMapType aHelpColorMap(aColorMap);
aColorMap.clear();
for (auto it = aHelpColorMap.begin(); it != aHelpColorMap.end(); ++it)
{
aColorMap.insert(std::pair{ (*it).first / 2, (*it).second });
aColorMap.insert(std::pair{ 100000 - (*it).first / 2, (*it).second });
}
}
else if ((rbHasColorGradient && aColorGradient.Style != awt::GradientStyle_LINEAR)
|| (!rbHasColorGradient && rbHasTransparenceGradient
&& aTransparenceGradient.Style != awt::GradientStyle_LINEAR))
{
// only LINEAR has same direction as Word, the others are reverse.
ColorMapType aHelpColorMap(aColorMap);
aColorMap.clear();
for (auto it = aHelpColorMap.begin(); it != aHelpColorMap.end(); ++it)
{
aColorMap.insert(std::pair{ 100000 - (*it).first, (*it).second });
}
}
// 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(
@@ -1110,6 +1422,18 @@ void FontworkHelpers::createCharInteropGrabBagUpdatesFromShapeProps(
drawing::FillStyle eFillStyle = drawing::FillStyle_SOLID;
if (xPropSetInfo->hasPropertyByName(u"FillStyle"))
rXPropSet->getPropertyValue(u"FillStyle") >>= eFillStyle;
// We might have a solid fill but a transparency gradient. That needs to be exported as gradFill
// too, because Word has transparency not separat 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")
&& (rXPropSet->getPropertyValue(u"FillTransparenceGradientName")
>>= sTransparenceGradientName)
&& !sTransparenceGradientName.isEmpty())
eFillStyle = drawing::FillStyle_GRADIENT;
switch (eFillStyle)
{
case drawing::FillStyle_NONE:
@@ -1117,15 +1441,101 @@ void FontworkHelpers::createCharInteropGrabBagUpdatesFromShapeProps(
pGrabBagStack->appendElement("noFill", uno::Any());
break;
}
case drawing::FillStyle_GRADIENT: // ToDo
case drawing::FillStyle_GRADIENT:
{
// fallback
pGrabBagStack->push("solidFill");
pGrabBagStack->push("srgbClr");
pGrabBagStack->push("attributes");
::Color aColor(ColorTransparency, 7512015); // LO default fill
pGrabBagStack->addString("val", aColor.AsRGBHexString());
// pop() calls are in the final getRootProperty() method
awt::Gradient aColorGradient;
bool bHasColorGradient(false);
awt::Gradient 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.getType() == 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 <w14:textFill> 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: