Coverage Report

Created: 2025-12-08 09:28

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libreoffice/oox/source/drawingml/connectorhelper.cxx
Line
Count
Source
1
/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
2
/*
3
 * This file is part of the LibreOffice project.
4
 *
5
 * This Source Code Form is subject to the terms of the Mozilla Public
6
 * License, v. 2.0. If a copy of the MPL was not distributed with this
7
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8
 */
9
10
#include <drawingml/connectorhelper.hxx>
11
12
#include <sal/config.h>
13
14
#include <com/sun/star/beans/XPropertySet.hpp>
15
#include <com/sun/star/container/XIdentifierContainer.hpp>
16
#include <com/sun/star/drawing/XGluePointsSupplier.hpp>
17
18
#include <basegfx/curve/b2dcubicbezier.hxx>
19
#include <basegfx/matrix/b2dhommatrix.hxx>
20
#include <basegfx/point/b2dpoint.hxx>
21
#include <basegfx/polygon/b2dpolypolygontools.hxx>
22
#include <basegfx/vector/b2dvector.hxx>
23
#include <drawingml/customshapeproperties.hxx>
24
#include <oox/drawingml/drawingmltypes.hxx>
25
#include <oox/drawingml/shape.hxx>
26
#include <rtl/ustring.hxx>
27
#include <svl/itempool.hxx>
28
#include <svx/svdmodel.hxx>
29
#include <svx/svdoedge.hxx>
30
#include <svx/svdobj.hxx>
31
#include <tools/mapunit.hxx>
32
#include <tools/UnitConversion.hxx>
33
34
#include <map>
35
#include <set>
36
#include <string_view>
37
#include <vector>
38
39
using namespace ::com::sun::star;
40
41
// These shapes have no gluepoints defined in their mso_CustomShape struct, thus the gluepoint
42
// adaption to default gluepoints will be done. Other shapes having no gluepoint defined in the
43
// mso_CustomShape struct, have gluepoints in order top-left-bottom-right in OOXML. But the shapes
44
// below have order right-bottom-left-top. Adding gluepoints to mso_CustomShape structs does not
45
// solve the problem because MS binary gluepoints and OOXML gluepoints are different.
46
47
bool ConnectorHelper::hasClockwiseCxn(const OUString& rShapeType)
48
26
{
49
26
    static const std::set<OUString> aWithClockwiseCxnSet({ u"accentBorderCallout1"_ustr,
50
26
                                                           u"accentBorderCallout2"_ustr,
51
26
                                                           u"accentBorderCallout3"_ustr,
52
26
                                                           u"accentCallout1"_ustr,
53
26
                                                           u"accentCallout2"_ustr,
54
26
                                                           u"accentCallout3"_ustr,
55
26
                                                           u"actionButtonBackPrevious"_ustr,
56
26
                                                           u"actionButtonBeginning"_ustr,
57
26
                                                           u"actionButtonBlank"_ustr,
58
26
                                                           u"actionButtonDocument"_ustr,
59
26
                                                           u"actionButtonEnd"_ustr,
60
26
                                                           u"actionButtonForwardNext"_ustr,
61
26
                                                           u"actionButtonHelp"_ustr,
62
26
                                                           u"actionButtonHome"_ustr,
63
26
                                                           u"actionButtonInformation"_ustr,
64
26
                                                           u"actionButtonMovie"_ustr,
65
26
                                                           u"actionButtonReturn"_ustr,
66
26
                                                           u"actionButtonSound"_ustr,
67
26
                                                           u"borderCallout1"_ustr,
68
26
                                                           u"borderCallout2"_ustr,
69
26
                                                           u"borderCallout3"_ustr,
70
26
                                                           u"callout1"_ustr,
71
26
                                                           u"callout2"_ustr,
72
26
                                                           u"callout3"_ustr,
73
26
                                                           u"cloud"_ustr,
74
26
                                                           u"corner"_ustr,
75
26
                                                           u"diagStripe"_ustr,
76
26
                                                           u"flowChartOfflineStorage"_ustr,
77
26
                                                           u"halfFrame"_ustr,
78
26
                                                           u"mathDivide"_ustr,
79
26
                                                           u"mathMinus"_ustr,
80
26
                                                           u"mathPlus"_ustr,
81
26
                                                           u"nonIsoscelesTrapezoid"_ustr,
82
26
                                                           u"pie"_ustr,
83
26
                                                           u"round2DiagRect"_ustr,
84
26
                                                           u"round2SameRect"_ustr,
85
26
                                                           u"snip1Rect"_ustr,
86
26
                                                           u"snip2DiagRect"_ustr,
87
26
                                                           u"snip2SameRect"_ustr,
88
26
                                                           u"snipRoundRect"_ustr });
89
26
    return aWithClockwiseCxnSet.contains(rShapeType);
90
26
}
91
92
basegfx::B2DHomMatrix
93
ConnectorHelper::getConnectorTransformMatrix(const oox::drawingml::ShapePtr& pConnector)
94
0
{
95
0
    basegfx::B2DHomMatrix aTransform; // ctor generates unit matrix
96
0
    if (!pConnector)
97
0
        return aTransform;
98
0
    if (pConnector->getFlipH())
99
0
        aTransform.scale(-1.0, 1.0);
100
0
    if (pConnector->getFlipV())
101
0
        aTransform.scale(1.0, -1.0);
102
0
    if (pConnector->getRotation() == 0)
103
0
        return aTransform;
104
105
0
    if (pConnector->getRotation() == 5400000 || pConnector->getRotation() == -16200000)
106
0
        aTransform *= basegfx::B2DHomMatrix(0, -1, 0, 1, 0, 0);
107
0
    else if (pConnector->getRotation() == 10800000 || pConnector->getRotation() == -10800000)
108
0
        aTransform *= basegfx::B2DHomMatrix(-1, 0, 0, 0, -1, 0);
109
0
    else if (pConnector->getRotation() == 16200000 || pConnector->getRotation() == -5400000)
110
0
        aTransform *= basegfx::B2DHomMatrix(0, 1, 0, -1, 0, 0);
111
0
    else
112
0
        SAL_WARN("oox", "tdf#157888 LibreOffice cannot handle such connector rotation");
113
0
    return aTransform;
114
0
}
115
116
void ConnectorHelper::getOOXHandlePositionsHmm(const oox::drawingml::ShapePtr& pConnector,
117
                                               std::vector<basegfx::B2DPoint>& rHandlePositions)
118
0
{
119
0
    rHandlePositions.clear();
120
121
0
    if (!pConnector)
122
0
        return;
123
124
0
    if (pConnector->getConnectorName() == u"bentConnector2"_ustr
125
0
        || pConnector->getConnectorName() == u"curvedConnector2"_ustr)
126
0
        return; // These have no handles.
127
128
    // Convert string attribute to number. Set default 50000 if missing.
129
0
    std::vector<sal_Int32> aAdjustmentOOXVec; // 1/100000 of shape size
130
0
    for (size_t i = 0; i < 3; i++)
131
0
    {
132
0
        if (i < pConnector->getConnectorAdjustments().size())
133
0
            aAdjustmentOOXVec.push_back(pConnector->getConnectorAdjustments()[i].toInt32());
134
0
        else
135
0
            aAdjustmentOOXVec.push_back(50000);
136
0
    }
137
138
    // Handle positions depend on EdgeKind and ShapeSize. bendConnector and curvedConnector use the
139
    // same handle positions. The formulas here correspond to guides in the bendConnector in
140
    // presetShapeDefinitions.xml.
141
0
    const double fWidth = pConnector->getSize().Width; // EMU
142
0
    const double fHeight = pConnector->getSize().Height; // EMU
143
0
    const double fPosX = pConnector->getPosition().X; // EMU
144
0
    const double fPosY = pConnector->getPosition().Y; // EMU
145
146
0
    if (pConnector->getConnectorName() == u"bentConnector3"_ustr
147
0
        || pConnector->getConnectorName() == u"curvedConnector3"_ustr)
148
0
    {
149
0
        double fAdj1 = aAdjustmentOOXVec[0];
150
0
        double fX1 = fAdj1 / 100000.0 * fWidth;
151
0
        double fY1 = fHeight / 2.0;
152
0
        rHandlePositions.push_back({ fX1, fY1 });
153
0
    }
154
0
    else if (pConnector->getConnectorName() == u"bentConnector4"_ustr
155
0
             || pConnector->getConnectorName() == u"curvedConnector4"_ustr)
156
0
    {
157
0
        double fAdj1 = aAdjustmentOOXVec[0];
158
0
        double fAdj2 = aAdjustmentOOXVec[1];
159
0
        double fX1 = fAdj1 / 100000.0 * fWidth;
160
0
        double fX2 = (fX1 + fWidth) / 2.0;
161
0
        double fY2 = fAdj2 / 100000.0 * fHeight;
162
0
        double fY1 = fY2 / 2.0;
163
0
        rHandlePositions.push_back({ fX1, fY1 });
164
0
        rHandlePositions.push_back({ fX2, fY2 });
165
0
    }
166
0
    else if (pConnector->getConnectorName() == u"bentConnector5"_ustr
167
0
             || pConnector->getConnectorName() == u"curvedConnector5"_ustr)
168
0
    {
169
0
        double fAdj1 = aAdjustmentOOXVec[0];
170
0
        double fAdj2 = aAdjustmentOOXVec[1];
171
0
        double fAdj3 = aAdjustmentOOXVec[2];
172
0
        double fX1 = fAdj1 / 100000.0 * fWidth;
173
0
        double fX3 = fAdj3 / 100000.0 * fWidth;
174
0
        double fX2 = (fX1 + fX3) / 2.0;
175
0
        double fY2 = fAdj2 / 100000.0 * fHeight;
176
0
        double fY1 = fY2 / 2.0;
177
0
        double fY3 = (fHeight + fY2) / 2.0;
178
0
        rHandlePositions.push_back({ fX1, fY1 });
179
0
        rHandlePositions.push_back({ fX2, fY2 });
180
0
        rHandlePositions.push_back({ fX3, fY3 });
181
0
    }
182
183
    // The presetGeometry has the first segment horizontal and start point left/top with
184
    // coordinates (0|0). Other layouts are done by flipping and rotating.
185
0
    basegfx::B2DHomMatrix aTransform;
186
0
    const basegfx::B2DPoint aB2DCenter(fWidth / 2.0, fHeight / 2.0);
187
0
    aTransform.translate(-aB2DCenter);
188
0
    aTransform *= getConnectorTransformMatrix(pConnector);
189
0
    aTransform.translate(aB2DCenter);
190
191
    // Make coordinates absolute
192
0
    aTransform.translate(fPosX, fPosY);
193
194
    // Actually transform the handle coordinates
195
0
    for (auto& rElem : rHandlePositions)
196
0
        rElem *= aTransform;
197
198
    // Convert EMU -> Hmm
199
0
    for (auto& rElem : rHandlePositions)
200
0
        rElem /= 360.0;
201
0
}
202
203
void ConnectorHelper::getLOBentHandlePositionsHmm(const oox::drawingml::ShapePtr& pConnector,
204
                                                  std::vector<basegfx::B2DPoint>& rHandlePositions)
205
0
{
206
    // This method is intended for Edgekind css::drawing::ConnectorType_STANDARD. Those connectors
207
    // correspond to OOX bentConnector, aka "ElbowConnector".
208
0
    rHandlePositions.clear();
209
210
0
    if (!pConnector)
211
0
        return;
212
0
    uno::Reference<drawing::XShape> xConnector(pConnector->getXShape());
213
0
    if (!xConnector.is())
214
0
        return;
215
216
    // Get the EdgeTrack polygon. We cannot use UNO "PolyPolygonBezier" because that includes
217
    // the yet not known anchor position in Writer. Thus get the polygon directly from the object.
218
0
    SdrEdgeObj* pEdgeObj = dynamic_cast<SdrEdgeObj*>(SdrObject::getSdrObjectFromXShape(xConnector));
219
0
    if (!pEdgeObj)
220
0
        return;
221
0
    basegfx::B2DPolyPolygon aB2DPolyPolygon(pEdgeObj->GetEdgeTrackPath());
222
0
    if (aB2DPolyPolygon.count() == 0)
223
0
        return;
224
225
0
    basegfx::B2DPolygon aEdgePolygon = aB2DPolyPolygon.getB2DPolygon(0);
226
0
    if (aEdgePolygon.count() < 4 || aEdgePolygon.areControlPointsUsed())
227
0
        return;
228
229
    // We need Hmm, the polygon might be e.g. in Twips, in Writer for example
230
0
    MapUnit eMapUnit = pEdgeObj->getSdrModelFromSdrObject().GetItemPool().GetMetric(0);
231
0
    if (eMapUnit != MapUnit::Map100thMM)
232
0
    {
233
0
        const auto eFrom = MapToO3tlLength(eMapUnit);
234
0
        if (eFrom == o3tl::Length::invalid)
235
0
            return;
236
0
        const double fConvert(o3tl::convert(1.0, eFrom, o3tl::Length::mm100));
237
0
        aEdgePolygon.transform(basegfx::B2DHomMatrix(fConvert, 0.0, 0.0, 0.0, fConvert, 0.0));
238
0
    }
239
240
    // LO has the handle in the middle of a segment, but not for first and last segment.
241
0
    for (sal_uInt32 i = 1; i < aEdgePolygon.count() - 2; i++)
242
0
    {
243
0
        const basegfx::B2DPoint aBeforePt(aEdgePolygon.getB2DPoint(i));
244
0
        const basegfx::B2DPoint aAfterPt(aEdgePolygon.getB2DPoint(i + 1));
245
0
        rHandlePositions.push_back((aBeforePt + aAfterPt) / 2.0);
246
0
    }
247
0
}
248
249
void ConnectorHelper::getLOCurvedHandlePositionsHmm(
250
    const oox::drawingml::ShapePtr& pConnector, std::vector<basegfx::B2DPoint>& rHandlePositions)
251
0
{
252
    // This method is intended for Edgekind css::drawing::ConnectorType_Curve for which OoXML
253
    // compatible routing is enabled.
254
0
    rHandlePositions.clear();
255
256
0
    if (!pConnector)
257
0
        return;
258
0
    uno::Reference<drawing::XShape> xConnector(pConnector->getXShape());
259
0
    if (!xConnector.is())
260
0
        return;
261
262
    // Get the EdgeTrack polygon. We cannot use UNO "PolyPolygonBezier" because that includes
263
    // the yet not known anchor position in Writer. Thus get the polygon directly from the object.
264
0
    SdrEdgeObj* pEdgeObj = dynamic_cast<SdrEdgeObj*>(SdrObject::getSdrObjectFromXShape(xConnector));
265
0
    if (!pEdgeObj)
266
0
        return;
267
0
    basegfx::B2DPolyPolygon aB2DPolyPolygon(pEdgeObj->GetEdgeTrackPath());
268
0
    if (aB2DPolyPolygon.count() == 0)
269
0
        return;
270
271
0
    basegfx::B2DPolygon aEdgePolygon = aB2DPolyPolygon.getB2DPolygon(0);
272
0
    if (aEdgePolygon.count() < 3 || !aEdgePolygon.areControlPointsUsed())
273
0
        return;
274
275
    // We need Hmm, the polygon might be e.g. in Twips, in Writer for example
276
0
    MapUnit eMapUnit = pEdgeObj->getSdrModelFromSdrObject().GetItemPool().GetMetric(0);
277
0
    if (eMapUnit != MapUnit::Map100thMM)
278
0
    {
279
0
        const auto eFrom = MapToO3tlLength(eMapUnit);
280
0
        if (eFrom == o3tl::Length::invalid)
281
0
            return;
282
0
        const double fConvert(o3tl::convert(1.0, eFrom, o3tl::Length::mm100));
283
0
        aEdgePolygon.transform(basegfx::B2DHomMatrix(fConvert, 0.0, 0.0, 0.0, fConvert, 0.0));
284
0
    }
285
286
    // The OOXML compatible routing has the handles as polygon points, but not start or
287
    // end point.
288
0
    for (sal_uInt32 i = 1; i < aEdgePolygon.count() - 1; i++)
289
0
    {
290
0
        rHandlePositions.push_back(aEdgePolygon.getB2DPoint(i));
291
0
    }
292
0
}
293
294
void ConnectorHelper::applyConnections(const oox::drawingml::ShapePtr& pConnector,
295
                                       oox::drawingml::ShapeIdMap& rShapeMap)
296
133
{
297
133
    uno::Reference<drawing::XShape> xConnector(pConnector->getXShape());
298
133
    if (!xConnector.is())
299
0
        return;
300
133
    uno::Reference<beans::XPropertySet> xPropSet(xConnector, uno::UNO_QUERY);
301
133
    if (!xPropSet.is())
302
0
        return;
303
304
    // MS Office allows route between shapes with small distance. LO default is 5mm.
305
133
    xPropSet->setPropertyValue(u"EdgeNode1HorzDist"_ustr, uno::Any(sal_Int32(0)));
306
133
    xPropSet->setPropertyValue(u"EdgeNode1VertDist"_ustr, uno::Any(sal_Int32(0)));
307
133
    xPropSet->setPropertyValue(u"EdgeNode2HorzDist"_ustr, uno::Any(sal_Int32(0)));
308
133
    xPropSet->setPropertyValue(u"EdgeNode2VertDist"_ustr, uno::Any(sal_Int32(0)));
309
310
    // A OOXML curvedConnector uses a routing method which is basically incompatible with the
311
    // traditional way of LibreOffice. A compatible way was added and needs to be enabled before
312
    // connections are set, so that the method is used in the default routing.
313
133
    xPropSet->setPropertyValue(u"EdgeOOXMLCurve"_ustr, uno::Any(true));
314
315
133
    oox::drawingml::ConnectorShapePropertiesList aConnectorShapeProperties
316
133
        = pConnector->getConnectorShapeProperties();
317
    // It contains maximal two items, each a struct with mbStartShape, maDestShapeId, mnDestGlueId
318
133
    for (const auto& aIt : aConnectorShapeProperties)
319
26
    {
320
26
        const auto pItem = rShapeMap.find(aIt.maDestShapeId);
321
26
        if (pItem == rShapeMap.end())
322
0
            continue;
323
324
26
        uno::Reference<drawing::XShape> xShape(pItem->second->getXShape(), uno::UNO_QUERY);
325
26
        if (xShape.is())
326
26
        {
327
            // Connect to the found shape.
328
26
            if (aIt.mbStartShape)
329
13
                xPropSet->setPropertyValue(u"StartShape"_ustr, uno::Any(xShape));
330
13
            else
331
13
                xPropSet->setPropertyValue(u"EndShape"_ustr, uno::Any(xShape));
332
333
            // The first four glue points are the default glue points, which are set by LibreOffice.
334
            // They do not belong to the preset geometry of the shape.
335
            // Adapt gluepoint index to LibreOffice
336
26
            uno::Reference<drawing::XGluePointsSupplier> xSupplier(xShape, uno::UNO_QUERY);
337
26
            css::uno::Reference<css::container::XIdentifierContainer> xGluePoints(
338
26
                xSupplier->getGluePoints(), uno::UNO_QUERY);
339
26
            sal_Int32 nCountGluePoints = xGluePoints->getIdentifiers().getLength();
340
26
            sal_Int32 nGlueId = aIt.mnDestGlueId;
341
342
26
            if (nCountGluePoints > 4)
343
0
                nGlueId += 4;
344
26
            else
345
26
            {
346
                // In these cases the mso_CustomShape struct defines no gluepoints (Why not?), thus
347
                // our default gluepoints are used. The order of the default gluepoints might differ
348
                // from the order of the OOXML gluepoints. We try to change nGlueId so, that the
349
                // connector attaches to a default gluepoint at the same side as it attaches in OOXML.
350
26
                const OUString sShapeType
351
26
                    = pItem->second->getCustomShapeProperties()->getShapePresetTypeName();
352
26
                if (ConnectorHelper::hasClockwiseCxn(sShapeType))
353
0
                    nGlueId = (nGlueId + 1) % 4;
354
26
                else
355
26
                {
356
26
                    bool bFlipH = pItem->second->getFlipH();
357
26
                    bool bFlipV = pItem->second->getFlipV();
358
26
                    if (bFlipH == bFlipV)
359
26
                    {
360
                        // change id of the left and right glue points of the bounding box (1 <-> 3)
361
26
                        if (nGlueId == 1)
362
6
                            nGlueId = 3; // Right
363
20
                        else if (nGlueId == 3)
364
20
                            nGlueId = 1; // Left
365
26
                    }
366
26
                }
367
26
            }
368
369
26
            if (aIt.mbStartShape)
370
13
                xPropSet->setPropertyValue(u"StartGluePointIndex"_ustr, uno::Any(nGlueId));
371
13
            else
372
13
                xPropSet->setPropertyValue(u"EndGluePointIndex"_ustr, uno::Any(nGlueId));
373
26
        }
374
26
    }
375
133
}
376
377
void ConnectorHelper::applyBentHandleAdjustments(oox::drawingml::ShapePtr pConnector)
378
0
{
379
0
    uno::Reference<drawing::XShape> xConnector(pConnector->getXShape(), uno::UNO_QUERY);
380
0
    if (!xConnector.is())
381
0
        return;
382
0
    uno::Reference<beans::XPropertySet> xPropSet(xConnector, uno::UNO_QUERY);
383
0
    if (!xPropSet.is())
384
0
        return;
385
386
0
    std::vector<basegfx::B2DPoint> aOOXMLHandles;
387
0
    ConnectorHelper::getOOXHandlePositionsHmm(pConnector, aOOXMLHandles);
388
0
    std::vector<basegfx::B2DPoint> aLODefaultHandles;
389
0
    ConnectorHelper::getLOBentHandlePositionsHmm(pConnector, aLODefaultHandles);
390
391
0
    if (aOOXMLHandles.size() == aLODefaultHandles.size())
392
0
    {
393
0
        bool bUseYforHori
394
0
            = basegfx::fTools::equalZero(getConnectorTransformMatrix(pConnector).get(0, 0));
395
0
        for (size_t i = 0; i < aOOXMLHandles.size(); i++)
396
0
        {
397
0
            basegfx::B2DVector aDiff(aOOXMLHandles[i] - aLODefaultHandles[i]);
398
0
            sal_Int32 nDiff;
399
0
            if ((i == 1 && !bUseYforHori) || (i != 1 && bUseYforHori))
400
0
                nDiff = basegfx::fround(aDiff.getY());
401
0
            else
402
0
                nDiff = basegfx::fround(aDiff.getX());
403
0
            xPropSet->setPropertyValue("EdgeLine" + OUString::number(i + 1) + "Delta",
404
0
                                       uno::Any(nDiff));
405
0
        }
406
0
    }
407
0
}
408
409
void ConnectorHelper::applyCurvedHandleAdjustments(oox::drawingml::ShapePtr pConnector)
410
0
{
411
0
    uno::Reference<drawing::XShape> xConnector(pConnector->getXShape(), uno::UNO_QUERY);
412
0
    if (!xConnector.is())
413
0
        return;
414
0
    uno::Reference<beans::XPropertySet> xPropSet(xConnector, uno::UNO_QUERY);
415
0
    if (!xPropSet.is())
416
0
        return;
417
418
0
    std::vector<basegfx::B2DPoint> aOOXMLHandles;
419
0
    ConnectorHelper::getOOXHandlePositionsHmm(pConnector, aOOXMLHandles);
420
0
    std::vector<basegfx::B2DPoint> aLODefaultHandles;
421
0
    ConnectorHelper::getLOCurvedHandlePositionsHmm(pConnector, aLODefaultHandles);
422
423
0
    if (aOOXMLHandles.size() == aLODefaultHandles.size())
424
0
    {
425
0
        bool bUseYforHori
426
0
            = basegfx::fTools::equalZero(getConnectorTransformMatrix(pConnector).get(0, 0));
427
0
        for (size_t i = 0; i < aOOXMLHandles.size(); i++)
428
0
        {
429
0
            basegfx::B2DVector aDiff(aOOXMLHandles[i] - aLODefaultHandles[i]);
430
0
            sal_Int32 nDiff;
431
0
            if ((i == 1 && !bUseYforHori) || (i != 1 && bUseYforHori))
432
0
                nDiff = basegfx::fround(aDiff.getY());
433
0
            else
434
0
                nDiff = basegfx::fround(aDiff.getX());
435
0
            xPropSet->setPropertyValue("EdgeLine" + OUString::number(i + 1) + "Delta",
436
0
                                       uno::Any(nDiff));
437
0
        }
438
0
    }
439
0
}
440
441
/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */