/src/libreoffice/basegfx/source/polygon/b2dsvgpolypolygon.cxx
Line | Count | Source |
1 | | /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
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 | | * This file incorporates work covered by the following license notice: |
10 | | * |
11 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
12 | | * contributor license agreements. See the NOTICE file distributed |
13 | | * with this work for additional information regarding copyright |
14 | | * ownership. The ASF licenses this file to you under the Apache |
15 | | * License, Version 2.0 (the "License"); you may not use this file |
16 | | * except in compliance with the License. You may obtain a copy of |
17 | | * the License at http://www.apache.org/licenses/LICENSE-2.0 . |
18 | | */ |
19 | | |
20 | | #include <basegfx/polygon/b2dpolygontools.hxx> |
21 | | #include <basegfx/polygon/b2dpolypolygontools.hxx> |
22 | | #include <basegfx/polygon/b2dpolypolygon.hxx> |
23 | | #include <basegfx/matrix/b2dhommatrix.hxx> |
24 | | #include <basegfx/matrix/b2dhommatrixtools.hxx> |
25 | | #include <basegfx/vector/b2enums.hxx> |
26 | | |
27 | | #include <rtl/ustring.hxx> |
28 | | #include <sal/log.hxx> |
29 | | #include <rtl/math.hxx> |
30 | | #include <rtl/character.hxx> |
31 | | #include <stringconversiontools.hxx> |
32 | | |
33 | | namespace |
34 | | { |
35 | | |
36 | | void putCommandChar(OUStringBuffer& rBuffer,sal_Unicode& rLastSVGCommand, sal_Unicode aChar, bool bToLower,bool bVerbose) |
37 | 68 | { |
38 | 68 | const sal_Unicode aCommand = bToLower ? rtl::toAsciiLowerCase(aChar) : aChar; |
39 | | |
40 | 68 | if (bVerbose && rBuffer.getLength()) |
41 | 0 | rBuffer.append(' '); |
42 | | |
43 | 68 | if (bVerbose || rLastSVGCommand != aCommand) |
44 | 68 | { |
45 | 68 | rBuffer.append(aCommand); |
46 | 68 | rLastSVGCommand = aCommand; |
47 | 68 | } |
48 | 68 | } |
49 | | |
50 | | void putNumberChar(OUStringBuffer& rStr,double fValue, double fOldValue, bool bUseRelativeCoordinates,bool bVerbose) |
51 | 85 | { |
52 | 85 | if (bUseRelativeCoordinates) |
53 | 51 | fValue -= fOldValue; |
54 | | |
55 | 85 | const sal_Int32 aLen(rStr.getLength()); |
56 | 85 | if (bVerbose || (aLen && basegfx::internal::isOnNumberChar(rStr[aLen - 1], false) && fValue >= 0.0)) |
57 | 34 | rStr.append(' '); |
58 | | |
59 | 85 | rStr.append(fValue); |
60 | 85 | } |
61 | | |
62 | | } |
63 | | |
64 | | namespace basegfx::utils |
65 | | { |
66 | | bool PointIndex::operator<(const PointIndex& rComp) const |
67 | 0 | { |
68 | 0 | if(rComp.getPolygonIndex() == getPolygonIndex()) |
69 | 0 | { |
70 | 0 | return rComp.getPointIndex() < getPointIndex(); |
71 | 0 | } |
72 | | |
73 | 0 | return rComp.getPolygonIndex() < getPolygonIndex(); |
74 | 0 | } |
75 | | |
76 | | bool importFromSvgD( |
77 | | B2DPolyPolygon& o_rPolyPolygon, |
78 | | std::u16string_view rSvgDStatement, |
79 | | bool bHandleRelativeNextPointCompatible, |
80 | | PointIndexSet* pHelpPointIndexSet) |
81 | 18.9k | { |
82 | 18.9k | o_rPolyPolygon.clear(); |
83 | 18.9k | const sal_Int32 nLen(rSvgDStatement.size()); |
84 | 18.9k | sal_Int32 nPos(0); |
85 | 18.9k | double nLastX( 0.0 ); |
86 | 18.9k | double nLastY( 0.0 ); |
87 | 18.9k | B2DPolygon aCurrPoly; |
88 | | |
89 | | // skip initial whitespace |
90 | 18.9k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
91 | | |
92 | 462k | while(nPos < nLen) |
93 | 444k | { |
94 | 444k | bool bRelative(false); |
95 | 444k | const sal_Unicode aCurrChar(rSvgDStatement[nPos]); |
96 | | |
97 | 444k | if(o_rPolyPolygon.count() && !aCurrPoly.count() && aCurrChar != 'm' && aCurrChar != 'M') |
98 | 10.6k | { |
99 | | // we have a new sub-polygon starting, but without a 'moveto' command. |
100 | | // this requires to add the current point as start point to the polygon |
101 | | // (see SVG1.1 8.3.3 The "closepath" command) |
102 | 10.6k | aCurrPoly.append(B2DPoint(nLastX, nLastY)); |
103 | 10.6k | } |
104 | | |
105 | 444k | switch(aCurrChar) |
106 | 444k | { |
107 | 18.0k | case 'z' : |
108 | 18.2k | case 'Z' : |
109 | 18.2k | { |
110 | | // consume CurrChar and whitespace |
111 | 18.2k | nPos++; |
112 | 18.2k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
113 | | |
114 | | // create closed polygon and reset import values |
115 | 18.2k | if(aCurrPoly.count()) |
116 | 18.0k | { |
117 | 18.0k | if(!bHandleRelativeNextPointCompatible) |
118 | 17.9k | { |
119 | | // SVG defines that "the next subpath starts at the |
120 | | // same initial point as the current subpath", so set the |
121 | | // current point if we do not need to be compatible |
122 | 17.9k | nLastX = aCurrPoly.getB2DPoint(0).getX(); |
123 | 17.9k | nLastY = aCurrPoly.getB2DPoint(0).getY(); |
124 | 17.9k | } |
125 | | |
126 | 18.0k | aCurrPoly.setClosed(true); |
127 | 18.0k | o_rPolyPolygon.append(aCurrPoly); |
128 | 18.0k | aCurrPoly.clear(); |
129 | 18.0k | } |
130 | | |
131 | 18.2k | break; |
132 | 18.0k | } |
133 | | |
134 | 29.1k | case 'm' : |
135 | 32.0k | case 'M' : |
136 | 32.0k | { |
137 | | // create non-closed polygon and reset import values |
138 | 32.0k | if(aCurrPoly.count()) |
139 | 10.5k | { |
140 | 10.5k | o_rPolyPolygon.append(aCurrPoly); |
141 | 10.5k | aCurrPoly.clear(); |
142 | 10.5k | } |
143 | 32.0k | [[fallthrough]]; // to add coordinate data as 1st point of new polygon |
144 | 32.0k | } |
145 | 35.9k | case 'l' : |
146 | 39.2k | case 'L' : |
147 | 39.2k | { |
148 | 39.2k | if(aCurrChar == 'm' || aCurrChar == 'l') |
149 | 33.0k | { |
150 | 33.0k | bRelative = true; |
151 | 33.0k | } |
152 | | |
153 | | // consume CurrChar and whitespace |
154 | 39.2k | nPos++; |
155 | 39.2k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
156 | | |
157 | 69.6k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
158 | 30.7k | { |
159 | 30.7k | double nX, nY; |
160 | | |
161 | 30.7k | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
162 | 30.7k | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
163 | | |
164 | 30.3k | if(bRelative) |
165 | 30.2k | { |
166 | 30.2k | nX += nLastX; |
167 | 30.2k | nY += nLastY; |
168 | 30.2k | } |
169 | | |
170 | | // set last position |
171 | 30.3k | nLastX = nX; |
172 | 30.3k | nLastY = nY; |
173 | | |
174 | | // add point |
175 | 30.3k | aCurrPoly.append(B2DPoint(nX, nY)); |
176 | 30.3k | } |
177 | 38.8k | break; |
178 | 39.2k | } |
179 | | |
180 | 38.8k | case 'h' : |
181 | 17.6k | { |
182 | 17.6k | bRelative = true; |
183 | 17.6k | [[fallthrough]]; |
184 | 17.6k | } |
185 | 17.8k | case 'H' : |
186 | 17.8k | { |
187 | 17.8k | nPos++; |
188 | 17.8k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
189 | | |
190 | 28.6k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
191 | 10.7k | { |
192 | 10.7k | double nX, nY(nLastY); |
193 | | |
194 | 10.7k | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
195 | | |
196 | 10.7k | if(bRelative) |
197 | 10.7k | { |
198 | 10.7k | nX += nLastX; |
199 | 10.7k | } |
200 | | |
201 | | // set last position |
202 | 10.7k | nLastX = nX; |
203 | | |
204 | | // add point |
205 | 10.7k | aCurrPoly.append(B2DPoint(nX, nY)); |
206 | 10.7k | } |
207 | 17.8k | break; |
208 | 17.8k | } |
209 | | |
210 | 17.8k | case 'v' : |
211 | 52 | { |
212 | 52 | bRelative = true; |
213 | 52 | [[fallthrough]]; |
214 | 52 | } |
215 | 64 | case 'V' : |
216 | 64 | { |
217 | 64 | nPos++; |
218 | 64 | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
219 | | |
220 | 103 | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
221 | 39 | { |
222 | 39 | double nX(nLastX), nY; |
223 | | |
224 | 39 | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
225 | | |
226 | 39 | if(bRelative) |
227 | 34 | { |
228 | 34 | nY += nLastY; |
229 | 34 | } |
230 | | |
231 | | // set last position |
232 | 39 | nLastY = nY; |
233 | | |
234 | | // add point |
235 | 39 | aCurrPoly.append(B2DPoint(nX, nY)); |
236 | 39 | } |
237 | 64 | break; |
238 | 64 | } |
239 | | |
240 | 10.7k | case 's' : |
241 | 10.7k | { |
242 | 10.7k | bRelative = true; |
243 | 10.7k | [[fallthrough]]; |
244 | 10.7k | } |
245 | 10.8k | case 'S' : |
246 | 10.8k | { |
247 | 10.8k | nPos++; |
248 | 10.8k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
249 | | |
250 | 10.8k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
251 | 167 | { |
252 | 167 | double nX, nY; |
253 | 167 | double nX2, nY2; |
254 | | |
255 | 167 | if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false; |
256 | 167 | if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false; |
257 | 139 | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
258 | 5 | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
259 | | |
260 | 3 | if(bRelative) |
261 | 2 | { |
262 | 2 | nX2 += nLastX; |
263 | 2 | nY2 += nLastY; |
264 | 2 | nX += nLastX; |
265 | 2 | nY += nLastY; |
266 | 2 | } |
267 | | |
268 | | // ensure existence of start point |
269 | 3 | sal_uInt32 nCurrPolyCount = aCurrPoly.count(); |
270 | 3 | if (nCurrPolyCount == 0) |
271 | 2 | { |
272 | 2 | aCurrPoly.append(B2DPoint(nLastX, nLastY)); |
273 | 2 | nCurrPolyCount = 1; |
274 | 2 | } |
275 | 3 | assert(nCurrPolyCount > 0 && "coverity 2023.12.2"); |
276 | | |
277 | | // get first control point. It's the reflection of the PrevControlPoint |
278 | | // of the last point. If not existent, use current point (see SVG) |
279 | 3 | B2DPoint aPrevControl(nLastX, nLastY); |
280 | 3 | const sal_uInt32 nIndex(nCurrPolyCount - 1); |
281 | | |
282 | 3 | if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex)) |
283 | 0 | { |
284 | 0 | const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex)); |
285 | 0 | const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex)); |
286 | | |
287 | | // use mirrored previous control point |
288 | 0 | aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX()); |
289 | 0 | aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY()); |
290 | 0 | } |
291 | | |
292 | | // append curved edge |
293 | 3 | aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2, nY2), B2DPoint(nX, nY)); |
294 | | |
295 | | // set last position |
296 | 3 | nLastX = nX; |
297 | 3 | nLastY = nY; |
298 | 3 | } |
299 | 10.7k | break; |
300 | 10.8k | } |
301 | | |
302 | 10.7k | case 'c' : |
303 | 10.7k | { |
304 | 10.7k | bRelative = true; |
305 | 10.7k | [[fallthrough]]; |
306 | 10.7k | } |
307 | 10.8k | case 'C' : |
308 | 10.8k | { |
309 | 10.8k | nPos++; |
310 | 10.8k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
311 | | |
312 | 10.8k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
313 | 50 | { |
314 | 50 | double nX, nY; |
315 | 50 | double nX1, nY1; |
316 | 50 | double nX2, nY2; |
317 | | |
318 | 50 | if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false; |
319 | 50 | if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false; |
320 | 2 | if(!basegfx::internal::importDoubleAndSpaces(nX2, nPos, rSvgDStatement, nLen)) return false; |
321 | 1 | if(!basegfx::internal::importDoubleAndSpaces(nY2, nPos, rSvgDStatement, nLen)) return false; |
322 | 1 | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
323 | 0 | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
324 | | |
325 | 0 | if(bRelative) |
326 | 0 | { |
327 | 0 | nX1 += nLastX; |
328 | 0 | nY1 += nLastY; |
329 | 0 | nX2 += nLastX; |
330 | 0 | nY2 += nLastY; |
331 | 0 | nX += nLastX; |
332 | 0 | nY += nLastY; |
333 | 0 | } |
334 | | |
335 | | // ensure existence of start point |
336 | 0 | if(!aCurrPoly.count()) |
337 | 0 | { |
338 | 0 | aCurrPoly.append(B2DPoint(nLastX, nLastY)); |
339 | 0 | } |
340 | | |
341 | | // append curved edge |
342 | 0 | aCurrPoly.appendBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX2, nY2), B2DPoint(nX, nY)); |
343 | | |
344 | | // set last position |
345 | 0 | nLastX = nX; |
346 | 0 | nLastY = nY; |
347 | 0 | } |
348 | 10.7k | break; |
349 | 10.8k | } |
350 | | |
351 | | // #100617# quadratic beziers are imported as cubic ones |
352 | 10.7k | case 'q' : |
353 | 3.48k | { |
354 | 3.48k | bRelative = true; |
355 | 3.48k | [[fallthrough]]; |
356 | 3.48k | } |
357 | 3.48k | case 'Q' : |
358 | 3.48k | { |
359 | 3.48k | nPos++; |
360 | 3.48k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
361 | | |
362 | 6.82k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
363 | 3.57k | { |
364 | 3.57k | double nX, nY; |
365 | 3.57k | double nX1, nY1; |
366 | | |
367 | 3.57k | if(!basegfx::internal::importDoubleAndSpaces(nX1, nPos, rSvgDStatement, nLen)) return false; |
368 | 3.57k | if(!basegfx::internal::importDoubleAndSpaces(nY1, nPos, rSvgDStatement, nLen)) return false; |
369 | 3.40k | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
370 | 3.37k | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
371 | | |
372 | 3.33k | if(bRelative) |
373 | 3.33k | { |
374 | 3.33k | nX1 += nLastX; |
375 | 3.33k | nY1 += nLastY; |
376 | 3.33k | nX += nLastX; |
377 | 3.33k | nY += nLastY; |
378 | 3.33k | } |
379 | | |
380 | | // ensure existence of start point |
381 | 3.33k | if(!aCurrPoly.count()) |
382 | 3.31k | { |
383 | 3.31k | aCurrPoly.append(B2DPoint(nLastX, nLastY)); |
384 | 3.31k | } |
385 | | |
386 | | // append curved edge |
387 | 3.33k | aCurrPoly.appendQuadraticBezierSegment(B2DPoint(nX1, nY1), B2DPoint(nX, nY)); |
388 | | |
389 | | // set last position |
390 | 3.33k | nLastX = nX; |
391 | 3.33k | nLastY = nY; |
392 | 3.33k | } |
393 | 3.24k | break; |
394 | 3.48k | } |
395 | | |
396 | | // #100617# relative quadratic beziers are imported as cubic |
397 | 7.17k | case 't' : |
398 | 7.17k | { |
399 | 7.17k | bRelative = true; |
400 | 7.17k | [[fallthrough]]; |
401 | 7.17k | } |
402 | 14.2k | case 'T' : |
403 | 14.2k | { |
404 | 14.2k | nPos++; |
405 | 14.2k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
406 | | |
407 | 14.3k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
408 | 83 | { |
409 | 83 | double nX, nY; |
410 | | |
411 | 83 | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
412 | 83 | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
413 | | |
414 | 18 | if(bRelative) |
415 | 18 | { |
416 | 18 | nX += nLastX; |
417 | 18 | nY += nLastY; |
418 | 18 | } |
419 | | |
420 | | // ensure existence of start point |
421 | 18 | sal_uInt32 nCurrPolyCount = aCurrPoly.count(); |
422 | 18 | if (nCurrPolyCount == 0) |
423 | 1 | { |
424 | 1 | aCurrPoly.append(B2DPoint(nLastX, nLastY)); |
425 | 1 | nCurrPolyCount = 1; |
426 | 1 | } |
427 | 18 | assert(nCurrPolyCount > 0 && "coverity 2023.12.2"); |
428 | | |
429 | | // get first control point. It's the reflection of the PrevControlPoint |
430 | | // of the last point. If not existent, use current point (see SVG) |
431 | 18 | B2DPoint aPrevControl(nLastX, nLastY); |
432 | 18 | const sal_uInt32 nIndex(nCurrPolyCount - 1); |
433 | 18 | const B2DPoint aPrevPoint(aCurrPoly.getB2DPoint(nIndex)); |
434 | | |
435 | 18 | if(aCurrPoly.areControlPointsUsed() && aCurrPoly.isPrevControlPointUsed(nIndex)) |
436 | 0 | { |
437 | 0 | const B2DPoint aPrevControlPoint(aCurrPoly.getPrevControlPoint(nIndex)); |
438 | | |
439 | | // use mirrored previous control point |
440 | 0 | aPrevControl.setX((2.0 * aPrevPoint.getX()) - aPrevControlPoint.getX()); |
441 | 0 | aPrevControl.setY((2.0 * aPrevPoint.getY()) - aPrevControlPoint.getY()); |
442 | 0 | } |
443 | | |
444 | 18 | if(!aPrevControl.equal(aPrevPoint)) |
445 | 0 | { |
446 | | // there is a prev control point, and we have the already mirrored one |
447 | | // in aPrevControl. We also need the quadratic control point for this |
448 | | // new quadratic segment to calculate the 2nd cubic control point |
449 | 0 | const B2DPoint aQuadControlPoint( |
450 | 0 | ((3.0 * aPrevControl.getX()) - aPrevPoint.getX()) / 2.0, |
451 | 0 | ((3.0 * aPrevControl.getY()) - aPrevPoint.getY()) / 2.0); |
452 | | |
453 | | // calculate the cubic bezier coefficients from the quadratic ones. |
454 | 0 | const double nX2Prime((aQuadControlPoint.getX() * 2.0 + nX) / 3.0); |
455 | 0 | const double nY2Prime((aQuadControlPoint.getY() * 2.0 + nY) / 3.0); |
456 | | |
457 | | // append curved edge, use mirrored cubic control point directly |
458 | 0 | aCurrPoly.appendBezierSegment(aPrevControl, B2DPoint(nX2Prime, nY2Prime), B2DPoint(nX, nY)); |
459 | 0 | } |
460 | 18 | else |
461 | 18 | { |
462 | | // when no previous control, SVG says to use current point -> straight line. |
463 | | // Just add end point |
464 | 18 | aCurrPoly.append(B2DPoint(nX, nY)); |
465 | 18 | } |
466 | | |
467 | | // set last position |
468 | 18 | nLastX = nX; |
469 | 18 | nLastY = nY; |
470 | 18 | } |
471 | 14.2k | break; |
472 | 14.2k | } |
473 | | |
474 | 14.2k | case 'a' : |
475 | 3.90k | { |
476 | 3.90k | bRelative = true; |
477 | 3.90k | [[fallthrough]]; |
478 | 3.90k | } |
479 | 3.92k | case 'A' : |
480 | 3.92k | { |
481 | 3.92k | nPos++; |
482 | 3.92k | basegfx::internal::skipSpaces(nPos, rSvgDStatement, nLen); |
483 | | |
484 | 3.92k | while(nPos < nLen && basegfx::internal::isOnNumberChar(rSvgDStatement, nPos)) |
485 | 5 | { |
486 | 5 | double nX, nY; |
487 | 5 | double fRX, fRY, fPhi; |
488 | 5 | sal_Int32 bLargeArcFlag, bSweepFlag; |
489 | | |
490 | 5 | if(!basegfx::internal::importDoubleAndSpaces(fRX, nPos, rSvgDStatement, nLen)) return false; |
491 | 5 | if(!basegfx::internal::importDoubleAndSpaces(fRY, nPos, rSvgDStatement, nLen)) return false; |
492 | 1 | if(!basegfx::internal::importDoubleAndSpaces(fPhi, nPos, rSvgDStatement, nLen)) return false; |
493 | 1 | if(!basegfx::internal::importFlagAndSpaces(bLargeArcFlag, nPos, rSvgDStatement, nLen)) return false; |
494 | 0 | if(!basegfx::internal::importFlagAndSpaces(bSweepFlag, nPos, rSvgDStatement, nLen)) return false; |
495 | 0 | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgDStatement, nLen)) return false; |
496 | 0 | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgDStatement, nLen)) return false; |
497 | | |
498 | 0 | if(bRelative) |
499 | 0 | { |
500 | 0 | nX += nLastX; |
501 | 0 | nY += nLastY; |
502 | 0 | } |
503 | |
|
504 | 0 | if( rtl::math::approxEqual(nX, nLastX) && rtl::math::approxEqual(nY, nLastY) ) |
505 | 0 | continue; // start==end -> skip according to SVG spec |
506 | | |
507 | 0 | if( fRX == 0.0 || fRY == 0.0 ) |
508 | 0 | { |
509 | | // straight line segment according to SVG spec |
510 | 0 | aCurrPoly.append(B2DPoint(nX, nY)); |
511 | 0 | } |
512 | 0 | else |
513 | 0 | { |
514 | | // normalize according to SVG spec |
515 | 0 | fRX=fabs(fRX); fRY=fabs(fRY); |
516 | | |
517 | | // from the SVG spec, appendix F.6.4 |
518 | | |
519 | | // |x1'| |cos phi sin phi| |(x1 - x2)/2| |
520 | | // |y1'| = |-sin phi cos phi| |(y1 - y2)/2| |
521 | 0 | const B2DPoint p1(nLastX, nLastY); |
522 | 0 | const B2DPoint p2(nX, nY); |
523 | 0 | B2DHomMatrix aTransform(basegfx::utils::createRotateB2DHomMatrix( |
524 | 0 | -deg2rad(fPhi))); |
525 | |
|
526 | 0 | const B2DPoint p1_prime( aTransform * B2DPoint(((p1-p2)/2.0)) ); |
527 | | |
528 | | // ______________________________________ rx y1' |
529 | | // |cx'| + / rx^2 ry^2 - rx^2 y1'^2 - ry^2 x1^2 ry |
530 | | // |cy'| =-/ rx^2y1'^2 + ry^2 x1'^2 - ry x1' |
531 | | // rx |
532 | | // chose + if f_A != f_S |
533 | | // chose - if f_A = f_S |
534 | 0 | B2DPoint aCenter_prime; |
535 | 0 | const double fRadicant( |
536 | 0 | (fRX*fRX*fRY*fRY - fRX*fRX*p1_prime.getY()*p1_prime.getY() - fRY*fRY*p1_prime.getX()*p1_prime.getX())/ |
537 | 0 | (fRX*fRX*p1_prime.getY()*p1_prime.getY() + fRY*fRY*p1_prime.getX()*p1_prime.getX())); |
538 | 0 | if( fRadicant < 0.0 ) |
539 | 0 | { |
540 | | // no solution - according to SVG |
541 | | // spec, scale up ellipse |
542 | | // uniformly such that it passes |
543 | | // through end points (denominator |
544 | | // of radicant solved for fRY, |
545 | | // with s=fRX/fRY) |
546 | 0 | const double fRatio(fRX/fRY); |
547 | 0 | fRY=std::hypot(p1_prime.getY(), p1_prime.getX()/fRatio); |
548 | 0 | fRX=fRatio*fRY; |
549 | | |
550 | | // keep center_prime forced to (0,0) |
551 | 0 | } |
552 | 0 | else |
553 | 0 | { |
554 | 0 | const double fFactor( |
555 | 0 | (bLargeArcFlag==bSweepFlag ? -1.0 : 1.0) * |
556 | 0 | sqrt(fRadicant)); |
557 | | |
558 | | // actually calculate center_prime |
559 | 0 | aCenter_prime = B2DPoint( |
560 | 0 | fFactor*fRX*p1_prime.getY()/fRY, |
561 | 0 | -fFactor*fRY*p1_prime.getX()/fRX); |
562 | 0 | } |
563 | | |
564 | | // + u - v |
565 | | // angle(u,v) = arccos( ------------ ) (take the sign of (ux vy - uy vx)) |
566 | | // - ||u|| ||v|| |
567 | | |
568 | | // 1 | (x1' - cx')/rx | |
569 | | // theta1 = angle(( ), | | ) |
570 | | // 0 | (y1' - cy')/ry | |
571 | 0 | const B2DPoint aRadii(fRX,fRY); |
572 | 0 | double fTheta1( |
573 | 0 | B2DVector(1.0,0.0).angle( |
574 | 0 | (p1_prime-aCenter_prime)/aRadii)); |
575 | | |
576 | | // |1| | (-x1' - cx')/rx | |
577 | | // theta2 = angle( | | , | | ) |
578 | | // |0| | (-y1' - cy')/ry | |
579 | 0 | double fTheta2( |
580 | 0 | B2DVector(1.0,0.0).angle( |
581 | 0 | (-p1_prime-aCenter_prime)/aRadii)); |
582 | | |
583 | | // map both angles to [0,2pi) |
584 | 0 | fTheta1 = fmod(2*M_PI+fTheta1,2*M_PI); |
585 | 0 | fTheta2 = fmod(2*M_PI+fTheta2,2*M_PI); |
586 | | |
587 | | // make sure the large arc is taken |
588 | | // (since |
589 | | // createPolygonFromEllipseSegment() |
590 | | // normalizes to e.g. cw arc) |
591 | 0 | if( !bSweepFlag ) |
592 | 0 | std::swap(fTheta1,fTheta2); |
593 | | |
594 | | // finally, create bezier polygon from this |
595 | 0 | B2DPolygon aSegment( |
596 | 0 | utils::createPolygonFromUnitEllipseSegment( |
597 | 0 | fTheta1, fTheta2 )); |
598 | | |
599 | | // transform ellipse by rotation & move to final center |
600 | 0 | aTransform = basegfx::utils::createScaleB2DHomMatrix(fRX, fRY); |
601 | 0 | aTransform.translate(aCenter_prime.getX(), |
602 | 0 | aCenter_prime.getY()); |
603 | 0 | aTransform.rotate(deg2rad(fPhi)); |
604 | 0 | const B2DPoint aOffset((p1+p2)/2.0); |
605 | 0 | aTransform.translate(aOffset.getX(), |
606 | 0 | aOffset.getY()); |
607 | 0 | aSegment.transform(aTransform); |
608 | | |
609 | | // createPolygonFromEllipseSegment() |
610 | | // always creates arcs that are |
611 | | // positively oriented - flip polygon |
612 | | // if we swapped angles above |
613 | 0 | if( !bSweepFlag ) |
614 | 0 | aSegment.flip(); |
615 | | |
616 | | // remember PointIndex of evtl. added pure helper points |
617 | 0 | sal_uInt32 nPointIndex(aCurrPoly.count() + 1); |
618 | 0 | aCurrPoly.append(aSegment); |
619 | | |
620 | | // if asked for, mark pure helper points by adding them to the index list of |
621 | | // helper points |
622 | 0 | if(pHelpPointIndexSet && aCurrPoly.count() > 1) |
623 | 0 | { |
624 | 0 | const sal_uInt32 nPolyIndex(o_rPolyPolygon.count()); |
625 | |
|
626 | 0 | for(;nPointIndex + 1 < aCurrPoly.count(); nPointIndex++) |
627 | 0 | { |
628 | 0 | pHelpPointIndexSet->insert(PointIndex(nPolyIndex, nPointIndex)); |
629 | 0 | } |
630 | 0 | } |
631 | 0 | } |
632 | | |
633 | | // set last position |
634 | 0 | nLastX = nX; |
635 | 0 | nLastY = nY; |
636 | 0 | } |
637 | 3.91k | break; |
638 | 3.92k | } |
639 | | |
640 | 326k | default: |
641 | 326k | { |
642 | 326k | SAL_WARN("basegfx", "importFromSvgD(): skipping tags in svg:d element (unknown: \"" |
643 | 326k | << OUString(aCurrChar) |
644 | 326k | << "\")!"); |
645 | 326k | ++nPos; |
646 | 326k | break; |
647 | 326k | } |
648 | 444k | } |
649 | 444k | } |
650 | | |
651 | | // if there is polygon data, create non-closed polygon |
652 | 18.0k | if(aCurrPoly.count()) |
653 | 72 | { |
654 | 72 | o_rPolyPolygon.append(aCurrPoly); |
655 | 72 | } |
656 | | |
657 | 18.0k | return true; |
658 | 18.9k | } |
659 | | |
660 | | bool importFromSvgPoints( B2DPolygon& o_rPoly, |
661 | | std::u16string_view rSvgPointsAttribute ) |
662 | 108 | { |
663 | 108 | o_rPoly.clear(); |
664 | 108 | const sal_Int32 nLen(rSvgPointsAttribute.size()); |
665 | 108 | sal_Int32 nPos(0); |
666 | 108 | double nX, nY; |
667 | | |
668 | | // skip initial whitespace |
669 | 108 | basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen); |
670 | | |
671 | 429 | while(nPos < nLen) |
672 | 322 | { |
673 | 322 | if(!basegfx::internal::importDoubleAndSpaces(nX, nPos, rSvgPointsAttribute, nLen)) return false; |
674 | 322 | if(!basegfx::internal::importDoubleAndSpaces(nY, nPos, rSvgPointsAttribute, nLen)) return false; |
675 | | |
676 | | // add point |
677 | 321 | o_rPoly.append(B2DPoint(nX, nY)); |
678 | | |
679 | | // skip to next number, or finish |
680 | 321 | basegfx::internal::skipSpaces(nPos, rSvgPointsAttribute, nLen); |
681 | 321 | } |
682 | | |
683 | 107 | return true; |
684 | 108 | } |
685 | | |
686 | | OUString exportToSvgPoints( const B2DPolygon& rPoly ) |
687 | 277 | { |
688 | 277 | SAL_WARN_IF(rPoly.areControlPointsUsed(), "basegfx", "exportToSvgPoints: Only non-bezier polygons allowed (!)"); |
689 | 277 | const sal_uInt32 nPointCount(rPoly.count()); |
690 | 277 | OUStringBuffer aResult; |
691 | | |
692 | 833 | for(sal_uInt32 a(0); a < nPointCount; a++) |
693 | 556 | { |
694 | 556 | const basegfx::B2DPoint aPoint(rPoly.getB2DPoint(a)); |
695 | | |
696 | 556 | if(a) |
697 | 279 | { |
698 | 279 | aResult.append(' '); |
699 | 279 | } |
700 | | |
701 | 556 | aResult.append(OUString::number(aPoint.getX()) |
702 | 556 | + "," |
703 | 556 | + OUString::number(aPoint.getY())); |
704 | 556 | } |
705 | | |
706 | 277 | return aResult.makeStringAndClear(); |
707 | 277 | } |
708 | | |
709 | | OUString exportToSvgD( |
710 | | const B2DPolyPolygon& rPolyPolygon, |
711 | | bool bUseRelativeCoordinates, |
712 | | bool bDetectQuadraticBeziers, |
713 | | bool bHandleRelativeNextPointCompatible, |
714 | | bool bOOXMLMotionPath) |
715 | 17 | { |
716 | 17 | const sal_uInt32 nCount(rPolyPolygon.count()); |
717 | 17 | sal_uInt32 nCombinedPointCount = 0; |
718 | 34 | for(sal_uInt32 i(0); i < nCount; i++) |
719 | 17 | { |
720 | 17 | const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i)); |
721 | 17 | nCombinedPointCount += aPolygon.count(); |
722 | 17 | } |
723 | | |
724 | 17 | OUStringBuffer aResult(std::max<int>(nCombinedPointCount * 32,512)); |
725 | 17 | B2DPoint aCurrentSVGPosition(0.0, 0.0); // SVG assumes (0,0) as the initial current point |
726 | | |
727 | 34 | for(sal_uInt32 i(0); i < nCount; i++) |
728 | 17 | { |
729 | 17 | const B2DPolygon& aPolygon(rPolyPolygon.getB2DPolygon(i)); |
730 | 17 | const sal_uInt32 nPointCount(aPolygon.count()); |
731 | | |
732 | 17 | if(nPointCount) |
733 | 17 | { |
734 | 17 | const bool bPolyUsesControlPoints(aPolygon.areControlPointsUsed()); |
735 | 17 | const sal_uInt32 nEdgeCount(aPolygon.isClosed() ? nPointCount : nPointCount - 1); |
736 | 17 | sal_Unicode aLastSVGCommand(' '); // last SVG command char |
737 | 17 | B2DPoint aLeft, aRight; // for quadratic bezier test |
738 | | |
739 | | // handle polygon start point |
740 | 17 | B2DPoint aEdgeStart(aPolygon.getB2DPoint(0)); |
741 | 17 | bool bUseRelativeCoordinatesForFirstPoint(bUseRelativeCoordinates); |
742 | | |
743 | 17 | if(bHandleRelativeNextPointCompatible) |
744 | 17 | { |
745 | | // To get around the error that the start point for the next polygon is the |
746 | | // start point of the current one (and not the last as it was handled up to now) |
747 | | // do force to write an absolute 'M' command as start for the next polygon |
748 | 17 | bUseRelativeCoordinatesForFirstPoint = false; |
749 | 17 | } |
750 | | |
751 | | // Write 'moveto' and the 1st coordinates, set aLastSVGCommand to 'lineto' |
752 | 17 | putCommandChar(aResult, aLastSVGCommand, 'M', bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); |
753 | 17 | putNumberChar(aResult, aEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); |
754 | 17 | putNumberChar(aResult, aEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinatesForFirstPoint, bOOXMLMotionPath); |
755 | 17 | aLastSVGCommand = bUseRelativeCoordinatesForFirstPoint ? 'l' : 'L'; |
756 | 17 | aCurrentSVGPosition = aEdgeStart; |
757 | | |
758 | 68 | for(sal_uInt32 nIndex(0); nIndex < nEdgeCount; nIndex++) |
759 | 51 | { |
760 | | // prepare access to next point |
761 | 51 | const sal_uInt32 nNextIndex((nIndex + 1) % nPointCount); |
762 | 51 | const B2DPoint aEdgeEnd(aPolygon.getB2DPoint(nNextIndex)); |
763 | | |
764 | | // handle edge from (aEdgeStart, aEdgeEnd) using indices (nIndex, nNextIndex) |
765 | 51 | const bool bEdgeIsBezier(bPolyUsesControlPoints |
766 | 0 | && (aPolygon.isNextControlPointUsed(nIndex) || aPolygon.isPrevControlPointUsed(nNextIndex))); |
767 | | |
768 | 51 | if(bEdgeIsBezier) |
769 | 0 | { |
770 | | // handle bezier edge |
771 | 0 | const B2DPoint aControlEdgeStart(aPolygon.getNextControlPoint(nIndex)); |
772 | 0 | const B2DPoint aControlEdgeEnd(aPolygon.getPrevControlPoint(nNextIndex)); |
773 | 0 | bool bIsQuadraticBezier(false); |
774 | | |
775 | | // check continuity at current edge's start point. For SVG, do NOT use an |
776 | | // existing continuity since no 'S' or 's' statement should be written. At |
777 | | // import, that 'previous' control vector is not available. SVG documentation |
778 | | // says for interpretation: |
779 | | |
780 | | // "(If there is no previous command or if the previous command was |
781 | | // not a C, c, S or s, assume the first control point is coincident |
782 | | // with the current point.)" |
783 | | |
784 | | // That's what is done from our import, so avoid exporting it as first statement |
785 | | // is necessary. |
786 | 0 | const bool bSymmetricAtEdgeStart( |
787 | 0 | !bOOXMLMotionPath && nIndex != 0 |
788 | 0 | && aPolygon.getContinuityInPoint(nIndex) == B2VectorContinuity::C2); |
789 | |
|
790 | 0 | if(bDetectQuadraticBeziers) |
791 | 0 | { |
792 | | // check for quadratic beziers - that's |
793 | | // the case if both control points are in |
794 | | // the same place when they are prolonged |
795 | | // to the common quadratic control point |
796 | | |
797 | | // Left: P = (3P1 - P0) / 2 |
798 | | // Right: P = (3P2 - P3) / 2 |
799 | 0 | aLeft = B2DPoint((3.0 * aControlEdgeStart - aEdgeStart) / 2.0); |
800 | 0 | aRight= B2DPoint((3.0 * aControlEdgeEnd - aEdgeEnd) / 2.0); |
801 | 0 | bIsQuadraticBezier = aLeft.equal(aRight); |
802 | 0 | } |
803 | |
|
804 | 0 | if(bIsQuadraticBezier) |
805 | 0 | { |
806 | | // approximately equal, export as quadratic bezier |
807 | 0 | if(bSymmetricAtEdgeStart) |
808 | 0 | { |
809 | 0 | putCommandChar(aResult, aLastSVGCommand, 'T', bUseRelativeCoordinates, bOOXMLMotionPath); |
810 | |
|
811 | 0 | putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
812 | 0 | putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
813 | 0 | aCurrentSVGPosition = aEdgeEnd; |
814 | 0 | } |
815 | 0 | else |
816 | 0 | { |
817 | 0 | putCommandChar(aResult, aLastSVGCommand, 'Q', bUseRelativeCoordinates, bOOXMLMotionPath); |
818 | |
|
819 | 0 | putNumberChar(aResult, aLeft.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
820 | 0 | putNumberChar(aResult, aLeft.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
821 | 0 | putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
822 | 0 | putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
823 | 0 | aCurrentSVGPosition = aEdgeEnd; |
824 | 0 | } |
825 | 0 | } |
826 | 0 | else |
827 | 0 | { |
828 | | // export as cubic bezier |
829 | 0 | if(bSymmetricAtEdgeStart) |
830 | 0 | { |
831 | 0 | putCommandChar(aResult, aLastSVGCommand, 'S', bUseRelativeCoordinates, bOOXMLMotionPath); |
832 | |
|
833 | 0 | putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
834 | 0 | putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
835 | 0 | putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
836 | 0 | putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
837 | 0 | aCurrentSVGPosition = aEdgeEnd; |
838 | 0 | } |
839 | 0 | else |
840 | 0 | { |
841 | 0 | putCommandChar(aResult, aLastSVGCommand, 'C', bUseRelativeCoordinates, bOOXMLMotionPath); |
842 | |
|
843 | 0 | putNumberChar(aResult, aControlEdgeStart.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
844 | 0 | putNumberChar(aResult, aControlEdgeStart.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
845 | 0 | putNumberChar(aResult, aControlEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
846 | 0 | putNumberChar(aResult, aControlEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
847 | 0 | putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
848 | 0 | putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
849 | 0 | aCurrentSVGPosition = aEdgeEnd; |
850 | 0 | } |
851 | 0 | } |
852 | 0 | } |
853 | 51 | else |
854 | 51 | { |
855 | | // straight edge |
856 | 51 | if(nNextIndex == 0) |
857 | 17 | { |
858 | | // it's a closed polygon's last edge and it's not a bezier edge, so there is |
859 | | // no need to write it |
860 | 17 | } |
861 | 34 | else |
862 | 34 | { |
863 | 34 | const bool bXEqual(rtl::math::approxEqual(aEdgeStart.getX(), aEdgeEnd.getX())); |
864 | 34 | const bool bYEqual(rtl::math::approxEqual(aEdgeStart.getY(), aEdgeEnd.getY())); |
865 | | |
866 | 34 | if(bXEqual && bYEqual) |
867 | 0 | { |
868 | | // point is a double point; do not export at all |
869 | 0 | } |
870 | 34 | else if(bXEqual && !bOOXMLMotionPath) |
871 | 0 | { |
872 | | // export as vertical line |
873 | 0 | putCommandChar(aResult, aLastSVGCommand, 'V', bUseRelativeCoordinates, bOOXMLMotionPath); |
874 | |
|
875 | 0 | putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
876 | 0 | aCurrentSVGPosition = aEdgeEnd; |
877 | 0 | } |
878 | 34 | else if(bYEqual && !bOOXMLMotionPath) |
879 | 17 | { |
880 | | // export as horizontal line |
881 | 17 | putCommandChar(aResult, aLastSVGCommand, 'H', bUseRelativeCoordinates, bOOXMLMotionPath); |
882 | | |
883 | 17 | putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
884 | 17 | aCurrentSVGPosition = aEdgeEnd; |
885 | 17 | } |
886 | 17 | else |
887 | 17 | { |
888 | | // export as line |
889 | 17 | putCommandChar(aResult, aLastSVGCommand, 'L', bUseRelativeCoordinates, bOOXMLMotionPath); |
890 | | |
891 | 17 | putNumberChar(aResult, aEdgeEnd.getX(), aCurrentSVGPosition.getX(), bUseRelativeCoordinates, bOOXMLMotionPath); |
892 | 17 | putNumberChar(aResult, aEdgeEnd.getY(), aCurrentSVGPosition.getY(), bUseRelativeCoordinates, bOOXMLMotionPath); |
893 | 17 | aCurrentSVGPosition = aEdgeEnd; |
894 | 17 | } |
895 | 34 | } |
896 | 51 | } |
897 | | |
898 | | // prepare edge start for next loop step |
899 | 51 | aEdgeStart = aEdgeEnd; |
900 | 51 | } |
901 | | |
902 | | // close path if closed poly (Z and z are equivalent here, but looks nicer when case is matched) |
903 | 17 | if(aPolygon.isClosed()) |
904 | 17 | { |
905 | 17 | putCommandChar(aResult, aLastSVGCommand, 'Z', bUseRelativeCoordinates, bOOXMLMotionPath); |
906 | 17 | } |
907 | 0 | else if (bOOXMLMotionPath) |
908 | 0 | { |
909 | 0 | putCommandChar(aResult, aLastSVGCommand, 'E', bUseRelativeCoordinates, bOOXMLMotionPath); |
910 | 0 | } |
911 | | |
912 | 17 | if(!bHandleRelativeNextPointCompatible) |
913 | 0 | { |
914 | | // SVG defines that "the next subpath starts at the same initial point as the current subpath", |
915 | | // so set aCurrentSVGPosition to the 1st point of the current, now ended and written path |
916 | 0 | aCurrentSVGPosition = aPolygon.getB2DPoint(0); |
917 | 0 | } |
918 | 17 | } |
919 | 17 | } |
920 | | |
921 | 17 | return aResult.makeStringAndClear(); |
922 | 17 | } |
923 | | } |
924 | | |
925 | | /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |