1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
3 * This file is part of the LibreOffice project.
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/.
9 * This file incorporates work covered by the following license notice:
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 .
20 #include <svgcharacternode.hxx>
21 #include <svgstyleattributes.hxx>
22 #include <drawinglayer/primitive2d/textprimitive2d.hxx>
23 #include <drawinglayer/primitive2d/textlayoutdevice.hxx>
24 #include <drawinglayer/primitive2d/textbreakuphelper.hxx>
25 #include <drawinglayer/primitive2d/textdecoratedprimitive2d.hxx>
26 #include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
28 #include <o3tl/string_view.hxx>
29 #include <osl/diagnose.h>
31 using namespace drawinglayer::primitive2d
;
33 namespace svgio::svgreader
37 class localTextBreakupHelper
: public TextBreakupHelper
40 SvgTextPosition
& mrSvgTextPosition
;
43 /// allow user callback to allow changes to the new TextTransformation. Default
45 virtual bool allowChange(sal_uInt32 nCount
, basegfx::B2DHomMatrix
& rNewTransform
, sal_uInt32 nIndex
, sal_uInt32 nLength
) override
;
48 localTextBreakupHelper(
49 const TextSimplePortionPrimitive2D
& rSource
,
50 SvgTextPosition
& rSvgTextPosition
)
51 : TextBreakupHelper(rSource
),
52 mrSvgTextPosition(rSvgTextPosition
)
59 bool localTextBreakupHelper::allowChange(sal_uInt32
/*nCount*/, basegfx::B2DHomMatrix
& rNewTransform
, sal_uInt32
/*nIndex*/, sal_uInt32
/*nLength*/)
61 const double fRotation(mrSvgTextPosition
.consumeRotation());
65 const basegfx::B2DPoint
aBasePoint(rNewTransform
* basegfx::B2DPoint(0.0, 0.0));
67 rNewTransform
.translate(-aBasePoint
.getX(), -aBasePoint
.getY());
68 rNewTransform
.rotate(fRotation
);
69 rNewTransform
.translate(aBasePoint
.getX(), aBasePoint
.getY());
75 SvgCharacterNode::SvgCharacterNode(
76 SvgDocument
& rDocument
,
79 : SvgNode(SVGToken::Character
, rDocument
, pParent
),
80 maText(std::move(aText
)),
85 SvgCharacterNode::~SvgCharacterNode()
89 const SvgStyleAttributes
* SvgCharacterNode::getSvgStyleAttributes() const
91 // no own style, use parent's
94 return getParent()->getSvgStyleAttributes();
102 drawinglayer::attribute::FontAttribute
SvgCharacterNode::getFontAttribute(
103 const SvgStyleAttributes
& rSvgStyleAttributes
)
105 const SvgStringVector
& rFontFamilyVector
= rSvgStyleAttributes
.getFontFamily();
106 OUString
aFontFamily(u
"Times New Roman"_ustr
);
107 if(!rFontFamilyVector
.empty())
108 aFontFamily
=rFontFamilyVector
[0];
110 // #i122324# if the FontFamily name ends on ' embedded' it is probably a re-import
111 // of a SVG export with font embedding. Remove this to make font matching work. This
112 // is pretty safe since there should be no font family names ending on ' embedded'.
113 // Remove again when FontEmbedding is implemented in SVG import
114 if(aFontFamily
.endsWith(" embedded"))
116 aFontFamily
= aFontFamily
.copy(0, aFontFamily
.getLength() - 9);
119 const ::FontWeight
nFontWeight(getVclFontWeight(rSvgStyleAttributes
.getFontWeight()));
120 bool bItalic(FontStyle::italic
== rSvgStyleAttributes
.getFontStyle() || FontStyle::oblique
== rSvgStyleAttributes
.getFontStyle());
122 return drawinglayer::attribute::FontAttribute(
129 false/*bMonospaced*/,
132 false/*bBiDiStrong*/);
135 rtl::Reference
<BasePrimitive2D
> SvgCharacterNode::createSimpleTextPrimitive(
136 SvgTextPosition
& rSvgTextPosition
,
137 const SvgStyleAttributes
& rSvgStyleAttributes
) const
139 // prepare retval, index and length
140 rtl::Reference
<BasePrimitive2D
> pRetval
;
141 const sal_uInt32
nLength(getText().getLength());
145 const sal_uInt32
nIndex(0);
147 // prepare FontAttribute
148 const drawinglayer::attribute::FontAttribute
aFontAttribute(getFontAttribute(rSvgStyleAttributes
));
150 // prepare FontSizeNumber
151 double fFontWidth(rSvgStyleAttributes
.getFontSizeNumber().solve(*this));
152 double fFontHeight(fFontWidth
);
155 css::lang::Locale aLocale
;
157 // prepare TextLayouterDevice; use a larger font size for more linear size
158 // calculations. Similar to nTextSizeFactor in sd/source/ui/view/sdview.cxx
159 // (ViewRedirector::createRedirectedPrimitive2DSequence).
160 const double sizeFactor
= fFontHeight
< 50000 ? 50000 / fFontHeight
: 1.0;
161 TextLayouterDevice aTextLayouterDevice
;
162 aTextLayouterDevice
.setFontAttribute(aFontAttribute
, fFontWidth
* sizeFactor
, fFontHeight
* sizeFactor
, aLocale
);
165 ::std::vector
< double > aTextArray(rSvgTextPosition
.getX());
166 ::std::vector
< double > aDxArray(rSvgTextPosition
.getDx());
168 // Do nothing when X and Dx arrays are empty
169 if((!aTextArray
.empty() || !aDxArray
.empty()) && aTextArray
.size() < nLength
)
171 const sal_uInt32
nArray(aTextArray
.size());
174 if (!aTextArray
.empty())
176 if(rSvgTextPosition
.getParent() && rSvgTextPosition
.getParent()->getAbsoluteX())
178 fStartX
= rSvgTextPosition
.getParent()->getPosition().getX();
182 fStartX
= aTextArray
[nArray
- 1];
186 ::std::vector
< double > aExtendArray(aTextLayouterDevice
.getTextArray(getText(), nArray
, nLength
- nArray
));
187 double fComulativeDx(0.0);
189 aTextArray
.reserve(nLength
);
190 for(size_t a
= 0; a
< aExtendArray
.size(); ++a
)
192 if (a
< aDxArray
.size())
194 fComulativeDx
+= aDxArray
[a
];
196 aTextArray
.push_back(aExtendArray
[a
] / sizeFactor
+ fStartX
+ fComulativeDx
);
200 // get current TextPosition and TextWidth in units
201 basegfx::B2DPoint
aPosition(rSvgTextPosition
.getPosition());
202 double fTextWidth(aTextLayouterDevice
.getTextWidth(getText(), nIndex
, nLength
) / sizeFactor
);
204 // check for user-given TextLength
205 if(0.0 != rSvgTextPosition
.getTextLength()
206 && !basegfx::fTools::equal(fTextWidth
, rSvgTextPosition
.getTextLength()))
208 const double fFactor(rSvgTextPosition
.getTextLength() / fTextWidth
);
210 if(rSvgTextPosition
.getLengthAdjust())
212 // spacing, need to create and expand TextArray
213 if(aTextArray
.empty())
215 auto aExtendArray(aTextLayouterDevice
.getTextArray(getText(), nIndex
, nLength
));
216 aTextArray
.reserve(aExtendArray
.size());
217 for (auto n
: aExtendArray
)
218 aTextArray
.push_back(n
/ sizeFactor
);
221 for(auto &a
: aTextArray
)
228 // spacing and glyphs, just apply to FontWidth
229 fFontWidth
*= fFactor
;
232 fTextWidth
= rSvgTextPosition
.getTextLength();
236 TextAlign
aTextAlign(rSvgStyleAttributes
.getTextAlign());
238 // map TextAnchor to TextAlign, there seems not to be a difference
239 if(TextAnchor::notset
!= rSvgStyleAttributes
.getTextAnchor())
241 switch(rSvgStyleAttributes
.getTextAnchor())
243 case TextAnchor::start
:
245 aTextAlign
= TextAlign::left
;
248 case TextAnchor::middle
:
250 aTextAlign
= TextAlign::center
;
253 case TextAnchor::end
:
255 aTextAlign
= TextAlign::right
;
268 case TextAlign::right
:
270 aPosition
.setX(aPosition
.getX() - mpParentLine
->getTextLineWidth());
273 case TextAlign::center
:
275 aPosition
.setX(aPosition
.getX() - (mpParentLine
->getTextLineWidth() * 0.5));
278 case TextAlign::notset
:
279 case TextAlign::left
:
280 case TextAlign::justify
:
282 // TextAlign::notset, TextAlign::left: nothing to do
283 // TextAlign::justify is not clear currently; handle as TextAlign::left
288 // get DominantBaseline
289 const DominantBaseline
aDominantBaseline(rSvgStyleAttributes
.getDominantBaseline());
291 basegfx::B2DRange
aRange(aTextLayouterDevice
.getTextBoundRect(getText(), nIndex
, nLength
));
292 // apply DominantBaseline
293 switch(aDominantBaseline
)
295 case DominantBaseline::Middle
:
296 case DominantBaseline::Central
:
298 aPosition
.setY(aPosition
.getY() - aRange
.getCenterY() / sizeFactor
);
301 case DominantBaseline::Hanging
:
303 aPosition
.setY(aPosition
.getY() - aRange
.getMinY() / sizeFactor
);
306 default: // DominantBaseline::Auto
314 const BaselineShift
aBaselineShift(rSvgStyleAttributes
.getBaselineShift());
316 // apply BaselineShift
317 switch(aBaselineShift
)
319 case BaselineShift::Sub
:
321 aPosition
.setY(aPosition
.getY() + aTextLayouterDevice
.getUnderlineOffset() / sizeFactor
);
324 case BaselineShift::Super
:
326 aPosition
.setY(aPosition
.getY() + aTextLayouterDevice
.getOverlineOffset() / sizeFactor
);
329 case BaselineShift::Percentage
:
330 case BaselineShift::Length
:
332 const SvgNumber
aNumber(rSvgStyleAttributes
.getBaselineShiftNumber());
333 const double mfBaselineShift(aNumber
.solve(*this));
335 aPosition
.setY(aPosition
.getY() - mfBaselineShift
);
338 default: // BaselineShift::Baseline
346 basegfx::BColor
aFill(0, 0, 0);
347 if(rSvgStyleAttributes
.getFill())
348 aFill
= *rSvgStyleAttributes
.getFill();
351 double fFillOpacity
= 1.0;
352 if (rSvgStyleAttributes
.getFillOpacity().isSet())
354 fFillOpacity
= rSvgStyleAttributes
.getFillOpacity().getNumber();
357 // prepare TextTransformation
358 basegfx::B2DHomMatrix aTextTransform
;
360 aTextTransform
.scale(fFontWidth
, fFontHeight
);
361 aTextTransform
.translate(aPosition
.getX(), aPosition
.getY());
363 // check TextDecoration and if TextDecoratedPortionPrimitive2D is needed
364 const TextDecoration
aDeco(rSvgStyleAttributes
.getTextDecoration());
366 if(TextDecoration::underline
== aDeco
367 || TextDecoration::overline
== aDeco
368 || TextDecoration::line_through
== aDeco
)
370 // get the fill for decoration as described by SVG. We cannot
371 // have different stroke colors/definitions for those, though
372 const SvgStyleAttributes
* pDecoDef
= rSvgStyleAttributes
.getTextDecorationDefiningSvgStyleAttributes();
374 basegfx::BColor
aDecoColor(aFill
);
375 if(pDecoDef
&& pDecoDef
->getFill())
376 aDecoColor
= *pDecoDef
->getFill();
378 TextLine eFontOverline
= TEXT_LINE_NONE
;
379 if(TextDecoration::overline
== aDeco
)
380 eFontOverline
= TEXT_LINE_SINGLE
;
382 TextLine eFontUnderline
= TEXT_LINE_NONE
;
383 if(TextDecoration::underline
== aDeco
)
384 eFontUnderline
= TEXT_LINE_SINGLE
;
386 TextStrikeout eTextStrikeout
= TEXT_STRIKEOUT_NONE
;
387 if(TextDecoration::line_through
== aDeco
)
388 eTextStrikeout
= TEXT_STRIKEOUT_SINGLE
;
390 // create decorated text primitive
391 pRetval
= new TextDecoratedPortionPrimitive2D(
396 std::move(aTextArray
),
403 // extra props for decorated
411 TEXT_FONT_EMPHASIS_MARK_NONE
,
419 // create text primitive
420 pRetval
= new TextSimplePortionPrimitive2D(
425 std::move(aTextArray
),
432 if (fFillOpacity
!= 1.0)
434 pRetval
= new UnifiedTransparencePrimitive2D(
435 drawinglayer::primitive2d::Primitive2DContainer
{ pRetval
},
439 // advance current TextPosition
440 rSvgTextPosition
.setPosition(rSvgTextPosition
.getPosition() + basegfx::B2DVector(fTextWidth
, 0.0));
446 void SvgCharacterNode::decomposeTextWithStyle(
447 Primitive2DContainer
& rTarget
,
448 SvgTextPosition
& rSvgTextPosition
,
449 const SvgStyleAttributes
& rSvgStyleAttributes
) const
451 const Primitive2DReference
xRef(
452 createSimpleTextPrimitive(
454 rSvgStyleAttributes
));
456 if(!(xRef
.is() && (Visibility::visible
== rSvgStyleAttributes
.getVisibility())))
459 if(!rSvgTextPosition
.isRotated())
461 rTarget
.push_back(xRef
);
465 // need to apply rotations to each character as given
466 const TextSimplePortionPrimitive2D
* pCandidate
=
467 dynamic_cast< const TextSimplePortionPrimitive2D
* >(xRef
.get());
471 localTextBreakupHelper
alocalTextBreakupHelper(*pCandidate
, rSvgTextPosition
);
472 Primitive2DContainer aResult
= alocalTextBreakupHelper
.extractResult();
476 rTarget
.append(std::move(aResult
));
479 // also consume for the implied single space
480 rSvgTextPosition
.consumeRotation();
484 OSL_ENSURE(false, "Used primitive is not a text primitive (!)");
490 SvgCharacterNode::whiteSpaceHandling(SvgCharacterNode
* pPreviousCharacterNode
)
492 bool bIsDefault(XmlSpace::Default
== getXmlSpace());
493 // if xml:space="default" then remove all newline characters, otherwise convert them to space
494 // convert tab to space too
495 maText
= maText
.replaceAll(u
"\n", bIsDefault
? u
"" : u
" ").replaceAll(u
"\t", u
" ");
499 if (maText
.isEmpty())
501 // Ignore this empty node for the purpose of whitespace handling
502 return pPreviousCharacterNode
;
505 if (pPreviousCharacterNode
&& pPreviousCharacterNode
->mbHadTrailingSpace
)
507 // pPreviousCharacterNode->mbHadTrailingSpace implies its xml:space="default".
508 // Even if this xml:space="preserve" node is whitespace-only, the trailing space
509 // of the previous node is significant - restore it
510 pPreviousCharacterNode
->maText
+= " ";
516 bool bHadLeadingSpace
= maText
.startsWith(" ");
517 mbHadTrailingSpace
= maText
.endsWith(" "); // Only set for xml:space="default"
519 // strip of all leading and trailing spaces
520 // and consolidate contiguous space
521 maText
= consolidateContiguousSpace(maText
.trim());
523 if (pPreviousCharacterNode
)
525 if (pPreviousCharacterNode
->mbHadTrailingSpace
)
527 // pPreviousCharacterNode->mbHadTrailingSpace implies its xml:space="default".
528 // The previous node already has a pending trailing space.
529 if (maText
.isEmpty())
531 // Leading spaces in this empty node are insignificant.
532 // Ignore this empty node for the purpose of whitespace handling
533 return pPreviousCharacterNode
;
535 // The previous node's trailing space is significant - restore it. Note that
536 // it is incorrect to insert a space in this node instead: the spaces in
537 // different nodes may have different size
538 pPreviousCharacterNode
->maText
+= " ";
542 if (bHadLeadingSpace
)
544 // This possibly whitespace-only xml:space="default" node goes after another
545 // node either having xml:space="default", but without a trailing space; or
546 // having xml:space="preserve" (in that case, it's irrelevant if that node had
547 // any trailing spaces).
548 if (!maText
.isEmpty())
550 // The leading whitespace in this node is significant - restore it
551 maText
= " " + maText
;
553 // The trailing whitespace in this node may or may not be
554 // significant (it will be significant, if there will be more nodes). Keep it as
555 // it is (even empty), but return this, to participate in whitespace handling
560 // No previous node, or no leading/trailing space on the previous node's boundary: if
561 // this is whitespace-only, its whitespace is never significant
562 return maText
.isEmpty() ? pPreviousCharacterNode
: this;
565 void SvgCharacterNode::concatenate(std::u16string_view rText
)
570 void SvgCharacterNode::decomposeText(Primitive2DContainer
& rTarget
, SvgTextPosition
& rSvgTextPosition
) const
572 if(!getText().isEmpty())
574 const SvgStyleAttributes
* pSvgStyleAttributes
= getSvgStyleAttributes();
576 if(pSvgStyleAttributes
)
578 decomposeTextWithStyle(rTarget
, rSvgTextPosition
, *pSvgStyleAttributes
);
583 } // end of namespace svgio
585 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */