diff options
author | Regina Henschel <rb.henschel@t-online.de> | 2023-03-06 16:45:35 +0100 |
---|---|---|
committer | Miklos Vajna <vmiklos@collabora.com> | 2023-03-09 08:25:51 +0000 |
commit | d95a09c1fca70d658207b8c48761af32dd2df213 (patch) | |
tree | 76c5828969b3309cc2298430e81d92b0f880ca63 /oox | |
parent | 78b1631e9649402e29c906c7023f55ed2cbe84f9 (diff) |
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>
Diffstat (limited to 'oox')
-rw-r--r-- | oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt | bin | 0 -> 17469 bytes | |||
-rw-r--r-- | oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt | bin | 0 -> 48423 bytes | |||
-rw-r--r-- | oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt | bin | 0 -> 24421 bytes | |||
-rw-r--r-- | oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt | bin | 0 -> 21169 bytes | |||
-rw-r--r-- | oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt | bin | 0 -> 17882 bytes | |||
-rw-r--r-- | oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt | bin | 0 -> 20571 bytes | |||
-rw-r--r-- | oox/qa/unit/export.cxx | 282 | ||||
-rw-r--r-- | oox/source/drawingml/fontworkhelpers.cxx | 434 |
8 files changed, 704 insertions, 12 deletions
diff --git a/oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt Binary files differnew file mode 100644 index 000000000000..99fe4d8a49da --- /dev/null +++ b/oox/qa/unit/data/tdf51195_Fontwork_axialGradient.odt diff --git a/oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt Binary files differnew file mode 100644 index 000000000000..c036e13953c6 --- /dev/null +++ b/oox/qa/unit/data/tdf51195_Fontwork_ellipticalGradient.odt diff --git a/oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt Binary files differnew file mode 100644 index 000000000000..bc821db884d9 --- /dev/null +++ b/oox/qa/unit/data/tdf51195_Fontwork_linearGradient.odt diff --git a/oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt Binary files differnew file mode 100644 index 000000000000..746b60b4d31d --- /dev/null +++ b/oox/qa/unit/data/tdf51195_Fontwork_radialGradient.odt diff --git a/oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt Binary files differnew file mode 100644 index 000000000000..d4daee9632d6 --- /dev/null +++ b/oox/qa/unit/data/tdf51195_Fontwork_rectGradient.odt diff --git a/oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt b/oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt Binary files differnew file mode 100644 index 000000000000..86e71ba6ae41 --- /dev/null +++ b/oox/qa/unit/data/tdf51195_Fontwork_squareGradient.odt diff --git a/oox/qa/unit/export.cxx b/oox/qa/unit/export.cxx index 645ffe386dd1..5a90126bfbe1 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 c339eb2405d3..cebe7bcf707e 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: |