office-gobmx/svgio/source/svgreader/svgcharacternode.cxx
Mike Kaganski 6e6081b340 tdf#157103: fix SVG whitespace handling
Previous code tried to hack some tricks to restore whitespaces after
trimming them according to the xml:space attribute value. But it was
wrong in multiple ways.

1. The collapsed space must stay in the block where its start was.
   When a block ended with a space, then trimming the space from it,
   and adding to a next block, can change the size of the space.
2. The shift of a line (e.g., specifying x and y values) doesn't end
   the logical line. A space before such a shift must be kept by the
   same rules, as if there weren't a shift.
3. A block with xml:space="preserve" is treated as if it consists of
   all non-whitespace characters, even if its leading or trailing
   characters are spaces. I.e., a trailing space in a block before,
   or a leading space in a block after, should be collapsed into a
   single space, not removed - even when the space-preserving block
   itself is made invisible.

Change-Id: Ic778d1e9d6b9d0c342ea74ad78d44bb47bc8d708
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/166239
Tested-by: Mike Kaganski <mike.kaganski@collabora.com>
Reviewed-by: Mike Kaganski <mike.kaganski@collabora.com>
2024-04-20 12:16:08 +02:00

585 lines
23 KiB
C++

/* -*- 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 <svgcharacternode.hxx>
#include <svgstyleattributes.hxx>
#include <drawinglayer/primitive2d/textprimitive2d.hxx>
#include <drawinglayer/primitive2d/textlayoutdevice.hxx>
#include <drawinglayer/primitive2d/textbreakuphelper.hxx>
#include <drawinglayer/primitive2d/textdecoratedprimitive2d.hxx>
#include <drawinglayer/primitive2d/unifiedtransparenceprimitive2d.hxx>
#include <utility>
#include <o3tl/string_view.hxx>
#include <osl/diagnose.h>
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<BasePrimitive2D> SvgCharacterNode::createSimpleTextPrimitive(
SvgTextPosition& rSvgTextPosition,
const SvgStyleAttributes& rSvgStyleAttributes) const
{
// prepare retval, index and length
rtl::Reference<BasePrimitive2D> 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: */