/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * This file is part of the LibreOffice project. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This file incorporates work covered by the following license notice: * * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed * with this work for additional information regarding copyright * ownership. The ASF licenses this file to you under the Apache * License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of * the License at http://www.apache.org/licenses/LICENSE-2.0 . */ #include #include #include #include #include #include #include #include #include #include using namespace drawinglayer::primitive2d; namespace svgio::svgreader { namespace { class localTextBreakupHelper : public TextBreakupHelper { private: SvgTextPosition& mrSvgTextPosition; protected: /// allow user callback to allow changes to the new TextTransformation. Default /// does nothing. virtual bool allowChange(sal_uInt32 nCount, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 nIndex, sal_uInt32 nLength) override; public: localTextBreakupHelper( const TextSimplePortionPrimitive2D& rSource, SvgTextPosition& rSvgTextPosition) : TextBreakupHelper(rSource), mrSvgTextPosition(rSvgTextPosition) { } }; } bool localTextBreakupHelper::allowChange(sal_uInt32 /*nCount*/, basegfx::B2DHomMatrix& rNewTransform, sal_uInt32 /*nIndex*/, sal_uInt32 /*nLength*/) { const double fRotation(mrSvgTextPosition.consumeRotation()); if(0.0 != fRotation) { const basegfx::B2DPoint aBasePoint(rNewTransform * basegfx::B2DPoint(0.0, 0.0)); rNewTransform.translate(-aBasePoint.getX(), -aBasePoint.getY()); rNewTransform.rotate(fRotation); rNewTransform.translate(aBasePoint.getX(), aBasePoint.getY()); } return true; } SvgCharacterNode::SvgCharacterNode( SvgDocument& rDocument, SvgNode* pParent, OUString aText) : SvgNode(SVGToken::Character, rDocument, pParent), maText(std::move(aText)), mpParentLine(nullptr) { } SvgCharacterNode::~SvgCharacterNode() { } const SvgStyleAttributes* SvgCharacterNode::getSvgStyleAttributes() const { // no own style, use parent's if(getParent()) { return getParent()->getSvgStyleAttributes(); } else { return nullptr; } } drawinglayer::attribute::FontAttribute SvgCharacterNode::getFontAttribute( const SvgStyleAttributes& rSvgStyleAttributes) { const SvgStringVector& rFontFamilyVector = rSvgStyleAttributes.getFontFamily(); OUString aFontFamily("Times New Roman"); if(!rFontFamilyVector.empty()) aFontFamily=rFontFamilyVector[0]; // #i122324# if the FontFamily name ends on ' embedded' it is probably a re-import // of a SVG export with font embedding. Remove this to make font matching work. This // is pretty safe since there should be no font family names ending on ' embedded'. // Remove again when FontEmbedding is implemented in SVG import if(aFontFamily.endsWith(" embedded")) { aFontFamily = aFontFamily.copy(0, aFontFamily.getLength() - 9); } const ::FontWeight nFontWeight(getVclFontWeight(rSvgStyleAttributes.getFontWeight())); bool bItalic(FontStyle::italic == rSvgStyleAttributes.getFontStyle() || FontStyle::oblique == rSvgStyleAttributes.getFontStyle()); return drawinglayer::attribute::FontAttribute( aFontFamily, OUString(), nFontWeight, false/*bSymbol*/, false/*bVertical*/, bItalic, false/*bMonospaced*/, false/*bOutline*/, false/*bRTL*/, false/*bBiDiStrong*/); } rtl::Reference SvgCharacterNode::createSimpleTextPrimitive( SvgTextPosition& rSvgTextPosition, const SvgStyleAttributes& rSvgStyleAttributes) const { // prepare retval, index and length rtl::Reference pRetval; const sal_uInt32 nLength(getText().getLength()); if(nLength) { const sal_uInt32 nIndex(0); // prepare FontAttribute const drawinglayer::attribute::FontAttribute aFontAttribute(getFontAttribute(rSvgStyleAttributes)); // prepare FontSizeNumber double fFontWidth(rSvgStyleAttributes.getFontSizeNumber().solve(*this)); double fFontHeight(fFontWidth); // prepare locale css::lang::Locale aLocale; // prepare TextLayouterDevice; use a larger font size for more linear size // calculations. Similar to nTextSizeFactor in sd/source/ui/view/sdview.cxx // (ViewRedirector::createRedirectedPrimitive2DSequence). const double sizeFactor = fFontHeight < 50000 ? 50000 / fFontHeight : 1.0; TextLayouterDevice aTextLayouterDevice; aTextLayouterDevice.setFontAttribute(aFontAttribute, fFontWidth * sizeFactor, fFontHeight * sizeFactor, aLocale); // prepare TextArray ::std::vector< double > aTextArray(rSvgTextPosition.getX()); ::std::vector< double > aDxArray(rSvgTextPosition.getDx()); // Do nothing when X and Dx arrays are empty if((!aTextArray.empty() || !aDxArray.empty()) && aTextArray.size() < nLength) { const sal_uInt32 nArray(aTextArray.size()); double fStartX(0.0); if (!aTextArray.empty()) { if(rSvgTextPosition.getParent() && rSvgTextPosition.getParent()->getAbsoluteX()) { fStartX = rSvgTextPosition.getParent()->getPosition().getX(); } else { fStartX = aTextArray[nArray - 1]; } } ::std::vector< double > aExtendArray(aTextLayouterDevice.getTextArray(getText(), nArray, nLength - nArray)); double fComulativeDx(0.0); aTextArray.reserve(nLength); for(size_t a = 0; a < aExtendArray.size(); ++a) { if (a < aDxArray.size()) { fComulativeDx += aDxArray[a]; } aTextArray.push_back(aExtendArray[a] / sizeFactor + fStartX + fComulativeDx); } } // get current TextPosition and TextWidth in units basegfx::B2DPoint aPosition(rSvgTextPosition.getPosition()); double fTextWidth(aTextLayouterDevice.getTextWidth(getText(), nIndex, nLength) / sizeFactor); // check for user-given TextLength if(0.0 != rSvgTextPosition.getTextLength() && !basegfx::fTools::equal(fTextWidth, rSvgTextPosition.getTextLength())) { const double fFactor(rSvgTextPosition.getTextLength() / fTextWidth); if(rSvgTextPosition.getLengthAdjust()) { // spacing, need to create and expand TextArray if(aTextArray.empty()) { auto aExtendArray(aTextLayouterDevice.getTextArray(getText(), nIndex, nLength)); aTextArray.reserve(aExtendArray.size()); for (auto n : aExtendArray) aTextArray.push_back(n / sizeFactor); } for(auto &a : aTextArray) { a *= fFactor; } } else { // spacing and glyphs, just apply to FontWidth fFontWidth *= fFactor; } fTextWidth = rSvgTextPosition.getTextLength(); } // get TextAlign TextAlign aTextAlign(rSvgStyleAttributes.getTextAlign()); // map TextAnchor to TextAlign, there seems not to be a difference if(TextAnchor::notset != rSvgStyleAttributes.getTextAnchor()) { switch(rSvgStyleAttributes.getTextAnchor()) { case TextAnchor::start: { aTextAlign = TextAlign::left; break; } case TextAnchor::middle: { aTextAlign = TextAlign::center; break; } case TextAnchor::end: { aTextAlign = TextAlign::right; break; } default: { break; } } } // apply TextAlign switch(aTextAlign) { case TextAlign::right: { aPosition.setX(aPosition.getX() - mpParentLine->getTextLineWidth()); break; } case TextAlign::center: { aPosition.setX(aPosition.getX() - (mpParentLine->getTextLineWidth() * 0.5)); break; } case TextAlign::notset: case TextAlign::left: case TextAlign::justify: { // TextAlign::notset, TextAlign::left: nothing to do // TextAlign::justify is not clear currently; handle as TextAlign::left break; } } // get DominantBaseline const DominantBaseline aDominantBaseline(rSvgStyleAttributes.getDominantBaseline()); basegfx::B2DRange aRange(aTextLayouterDevice.getTextBoundRect(getText(), nIndex, nLength)); // apply DominantBaseline switch(aDominantBaseline) { case DominantBaseline::Middle: case DominantBaseline::Central: { aPosition.setY(aPosition.getY() - aRange.getCenterY() / sizeFactor); break; } case DominantBaseline::Hanging: { aPosition.setY(aPosition.getY() - aRange.getMinY() / sizeFactor); break; } default: // DominantBaseline::Auto { // nothing to do break; } } // get BaselineShift const BaselineShift aBaselineShift(rSvgStyleAttributes.getBaselineShift()); // apply BaselineShift switch(aBaselineShift) { case BaselineShift::Sub: { aPosition.setY(aPosition.getY() + aTextLayouterDevice.getUnderlineOffset() / sizeFactor); break; } case BaselineShift::Super: { aPosition.setY(aPosition.getY() + aTextLayouterDevice.getOverlineOffset() / sizeFactor); break; } case BaselineShift::Percentage: case BaselineShift::Length: { const SvgNumber aNumber(rSvgStyleAttributes.getBaselineShiftNumber()); const double mfBaselineShift(aNumber.solve(*this)); aPosition.setY(aPosition.getY() - mfBaselineShift); break; } default: // BaselineShift::Baseline { // nothing to do break; } } // get fill color basegfx::BColor aFill(0, 0, 0); if(rSvgStyleAttributes.getFill()) aFill = *rSvgStyleAttributes.getFill(); // get fill opacity double fFillOpacity = 1.0; if (rSvgStyleAttributes.getFillOpacity().isSet()) { fFillOpacity = rSvgStyleAttributes.getFillOpacity().getNumber(); } // prepare TextTransformation basegfx::B2DHomMatrix aTextTransform; aTextTransform.scale(fFontWidth, fFontHeight); aTextTransform.translate(aPosition.getX(), aPosition.getY()); // check TextDecoration and if TextDecoratedPortionPrimitive2D is needed const TextDecoration aDeco(rSvgStyleAttributes.getTextDecoration()); if(TextDecoration::underline == aDeco || TextDecoration::overline == aDeco || TextDecoration::line_through == aDeco) { // get the fill for decoration as described by SVG. We cannot // have different stroke colors/definitions for those, though const SvgStyleAttributes* pDecoDef = rSvgStyleAttributes.getTextDecorationDefiningSvgStyleAttributes(); basegfx::BColor aDecoColor(aFill); if(pDecoDef && pDecoDef->getFill()) aDecoColor = *pDecoDef->getFill(); TextLine eFontOverline = TEXT_LINE_NONE; if(TextDecoration::overline == aDeco) eFontOverline = TEXT_LINE_SINGLE; TextLine eFontUnderline = TEXT_LINE_NONE; if(TextDecoration::underline == aDeco) eFontUnderline = TEXT_LINE_SINGLE; TextStrikeout eTextStrikeout = TEXT_STRIKEOUT_NONE; if(TextDecoration::line_through == aDeco) eTextStrikeout = TEXT_STRIKEOUT_SINGLE; // create decorated text primitive pRetval = new TextDecoratedPortionPrimitive2D( aTextTransform, getText(), nIndex, nLength, std::move(aTextArray), {}, aFontAttribute, aLocale, aFill, COL_TRANSPARENT, // extra props for decorated aDecoColor, aDecoColor, eFontOverline, eFontUnderline, false, eTextStrikeout, false, TEXT_FONT_EMPHASIS_MARK_NONE, true, false, TEXT_RELIEF_NONE, false); } else { // create text primitive pRetval = new TextSimplePortionPrimitive2D( aTextTransform, getText(), nIndex, nLength, std::move(aTextArray), {}, aFontAttribute, aLocale, aFill); } if (fFillOpacity != 1.0) { pRetval = new UnifiedTransparencePrimitive2D( drawinglayer::primitive2d::Primitive2DContainer{ pRetval }, 1.0 - fFillOpacity); } // advance current TextPosition rSvgTextPosition.setPosition(rSvgTextPosition.getPosition() + basegfx::B2DVector(fTextWidth, 0.0)); } return pRetval; } void SvgCharacterNode::decomposeTextWithStyle( Primitive2DContainer& rTarget, SvgTextPosition& rSvgTextPosition, const SvgStyleAttributes& rSvgStyleAttributes) const { const Primitive2DReference xRef( createSimpleTextPrimitive( rSvgTextPosition, rSvgStyleAttributes)); if(!(xRef.is() && (Visibility::visible == rSvgStyleAttributes.getVisibility()))) return; if(!rSvgTextPosition.isRotated()) { rTarget.push_back(xRef); } else { // need to apply rotations to each character as given const TextSimplePortionPrimitive2D* pCandidate = dynamic_cast< const TextSimplePortionPrimitive2D* >(xRef.get()); if(pCandidate) { localTextBreakupHelper alocalTextBreakupHelper(*pCandidate, rSvgTextPosition); Primitive2DContainer aResult = alocalTextBreakupHelper.extractResult(); if(!aResult.empty()) { rTarget.append(std::move(aResult)); } // also consume for the implied single space rSvgTextPosition.consumeRotation(); } else { OSL_ENSURE(false, "Used primitive is not a text primitive (!)"); } } } SvgCharacterNode* SvgCharacterNode::whiteSpaceHandling(SvgCharacterNode* pPreviousCharacterNode) { bool bIsDefault(XmlSpace::Default == getXmlSpace()); // if xml:space="default" then remove all newline characters, otherwise convert them to space // convert tab to space too maText = maText.replaceAll(u"\n", bIsDefault ? u"" : u" ").replaceAll(u"\t", u" "); if (!bIsDefault) { if (maText.isEmpty()) { // Ignore this empty node for the purpose of whitespace handling return pPreviousCharacterNode; } if (pPreviousCharacterNode && pPreviousCharacterNode->mbHadTrailingSpace) { // pPreviousCharacterNode->mbHadTrailingSpace implies its xml:space="default". // Even if this xml:space="preserve" node is whitespace-only, the trailing space // of the previous node is significant - restore it pPreviousCharacterNode->maText += " "; } return this; } bool bHadLeadingSpace = maText.startsWith(" "); mbHadTrailingSpace = maText.endsWith(" "); // Only set for xml:space="default" // strip of all leading and trailing spaces // and consolidate contiguous space maText = consolidateContiguousSpace(maText.trim()); if (pPreviousCharacterNode) { if (pPreviousCharacterNode->mbHadTrailingSpace) { // pPreviousCharacterNode->mbHadTrailingSpace implies its xml:space="default". // The previous node already has a pending trailing space. if (maText.isEmpty()) { // Leading spaces in this empty node are insignificant. // Ignore this empty node for the purpose of whitespace handling return pPreviousCharacterNode; } // The previous node's trailing space is significant - restore it. Note that // it is incorrect to insert a space in this node instead: the spaces in // different nodes may have different size pPreviousCharacterNode->maText += " "; return this; } if (bHadLeadingSpace) { // This possibly whitespace-only xml:space="default" node goes after another // node either having xml:space="default", but without a trailing space; or // having xml:space="preserve" (in that case, it's irrelevant if that node had // any trailing spaces). if (!maText.isEmpty()) { // The leading whitespace in this node is significant - restore it maText = " " + maText; } // The trailing whitespace in this node may or may not be // significant (it will be significant, if there will be more nodes). Keep it as // it is (even empty), but return this, to participate in whitespace handling return this; } } // No previous node, or no leading/trailing space on the previous node's boundary: if // this is whitespace-only, its whitespace is never significant return maText.isEmpty() ? pPreviousCharacterNode : this; } void SvgCharacterNode::concatenate(std::u16string_view rText) { maText += rText; } void SvgCharacterNode::decomposeText(Primitive2DContainer& rTarget, SvgTextPosition& rSvgTextPosition) const { if(!getText().isEmpty()) { const SvgStyleAttributes* pSvgStyleAttributes = getSvgStyleAttributes(); if(pSvgStyleAttributes) { decomposeTextWithStyle(rTarget, rSvgTextPosition, *pSvgStyleAttributes); } } } } // end of namespace svgio /* vim:set shiftwidth=4 softtabstop=4 expandtab: */