QGIS/src/core/textrenderer/qgstextformat.cpp
Nyall Dawson 730081dceb Handle view device pixel ratio when generating preview icons
for text format and label settings
2023-06-21 13:46:50 +10:00

1202 lines
43 KiB
C++

/***************************************************************************
qgstextformat.cpp
---------------
begin : May 2020
copyright : (C) Nyall Dawson
email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgstextformat.h"
#include "qgstextrenderer_p.h"
#include "qgstextrenderer.h"
#include "qgsvectorlayer.h"
#include "qgsfontutils.h"
#include "qgssymbollayerutils.h"
#include "qgspainting.h"
#include "qgstextrendererutils.h"
#include "qgspallabeling.h"
#include "qgsconfig.h"
#include "qgsfontmanager.h"
#include "qgsapplication.h"
#include "qgsunittypes.h"
#include <QFontDatabase>
#include <QMimeData>
#include <QWidget>
#include <QScreen>
QgsTextFormat::QgsTextFormat()
{
d = new QgsTextSettingsPrivate();
}
QgsTextFormat::QgsTextFormat( const QgsTextFormat &other ) //NOLINT
: mBufferSettings( other.mBufferSettings )
, mBackgroundSettings( other.mBackgroundSettings )
, mShadowSettings( other.mShadowSettings )
, mMaskSettings( other.mMaskSettings )
, mTextFontFamily( other.mTextFontFamily )
, mTextFontFound( other.mTextFontFound )
, d( other.d )
{
}
QgsTextFormat &QgsTextFormat::operator=( const QgsTextFormat &other ) //NOLINT
{
d = other.d;
mBufferSettings = other.mBufferSettings;
mBackgroundSettings = other.mBackgroundSettings;
mShadowSettings = other.mShadowSettings;
mMaskSettings = other.mMaskSettings;
mTextFontFamily = other.mTextFontFamily;
mTextFontFound = other.mTextFontFound;
return *this;
}
QgsTextFormat::~QgsTextFormat() //NOLINT
{
}
bool QgsTextFormat::operator==( const QgsTextFormat &other ) const
{
if ( d->isValid != other.isValid()
|| d->textFont != other.font()
|| namedStyle() != other.namedStyle()
|| d->fontSizeUnits != other.sizeUnit()
|| d->fontSizeMapUnitScale != other.sizeMapUnitScale()
|| d->fontSize != other.size()
|| d->textColor != other.color()
|| d->opacity != other.opacity()
|| d->blendMode != other.blendMode()
|| d->multilineHeight != other.lineHeight()
|| d->multilineHeightUnits != other.lineHeightUnit()
|| d->orientation != other.orientation()
|| d->previewBackgroundColor != other.previewBackgroundColor()
|| d->allowHtmlFormatting != other.allowHtmlFormatting()
|| d->forcedBold != other.forcedBold()
|| d->forcedItalic != other.forcedItalic()
|| d->capitalization != other.capitalization()
|| mBufferSettings != other.mBufferSettings
|| mBackgroundSettings != other.mBackgroundSettings
|| mShadowSettings != other.mShadowSettings
|| mMaskSettings != other.mMaskSettings
|| d->families != other.families()
|| d->mDataDefinedProperties != other.dataDefinedProperties() )
return false;
return true;
}
bool QgsTextFormat::operator!=( const QgsTextFormat &other ) const
{
return !( *this == other );
}
bool QgsTextFormat::isValid() const
{
return d->isValid;
}
void QgsTextFormat::setValid()
{
d->isValid = true;
}
QgsTextBufferSettings &QgsTextFormat::buffer()
{
d->isValid = true;
return mBufferSettings;
}
void QgsTextFormat::setBuffer( const QgsTextBufferSettings &bufferSettings )
{
d->isValid = true;
mBufferSettings = bufferSettings;
}
QgsTextBackgroundSettings &QgsTextFormat::background()
{
d->isValid = true;
return mBackgroundSettings;
}
void QgsTextFormat::setBackground( const QgsTextBackgroundSettings &backgroundSettings )
{
d->isValid = true;
mBackgroundSettings = backgroundSettings;
}
QgsTextShadowSettings &QgsTextFormat::shadow()
{
d->isValid = true;
return mShadowSettings;
}
void QgsTextFormat::setShadow( const QgsTextShadowSettings &shadowSettings )
{
d->isValid = true;
mShadowSettings = shadowSettings;
}
QgsTextMaskSettings &QgsTextFormat::mask()
{
d->isValid = true;
return mMaskSettings;
}
void QgsTextFormat::setMask( const QgsTextMaskSettings &maskSettings )
{
d->isValid = true;
mMaskSettings = maskSettings;
}
QFont QgsTextFormat::font() const
{
return d->textFont;
}
QFont QgsTextFormat::scaledFont( const QgsRenderContext &context, double scaleFactor, bool *isZeroSize ) const
{
if ( isZeroSize )
*isZeroSize = false;
QFont font = d->textFont;
if ( scaleFactor == 1 )
{
int fontPixelSize = QgsTextRenderer::sizeToPixel( d->fontSize, context, d->fontSizeUnits,
d->fontSizeMapUnitScale );
if ( fontPixelSize == 0 )
{
if ( isZeroSize )
*isZeroSize = true;
return QFont();
}
font.setPixelSize( fontPixelSize );
}
else
{
double fontPixelSize = context.convertToPainterUnits( d->fontSize, d->fontSizeUnits, d->fontSizeMapUnitScale );
if ( qgsDoubleNear( fontPixelSize, 0 ) )
{
if ( isZeroSize )
*isZeroSize = true;
return QFont();
}
const int roundedPixelSize = static_cast< int >( std::round( scaleFactor * fontPixelSize + 0.5 ) );
font.setPixelSize( roundedPixelSize );
}
font.setLetterSpacing( QFont::AbsoluteSpacing, context.convertToPainterUnits( d->textFont.letterSpacing(), d->fontSizeUnits, d->fontSizeMapUnitScale ) * scaleFactor );
font.setWordSpacing( context.convertToPainterUnits( d->textFont.wordSpacing(), d->fontSizeUnits, d->fontSizeMapUnitScale ) * scaleFactor * scaleFactor );
if ( d->capitalization == Qgis::Capitalization::SmallCaps
|| d->capitalization == Qgis::Capitalization::AllSmallCaps )
font.setCapitalization( QFont::SmallCaps );
return font;
}
void QgsTextFormat::setFont( const QFont &font )
{
d->isValid = true;
d->textFont = font;
}
QString QgsTextFormat::namedStyle() const
{
if ( !d->textNamedStyle.isEmpty() )
return d->textNamedStyle;
QFontDatabase db;
return db.styleString( d->textFont );
}
void QgsTextFormat::setNamedStyle( const QString &style )
{
d->isValid = true;
QgsFontUtils::updateFontViaStyle( d->textFont, style );
d->textNamedStyle = style;
}
bool QgsTextFormat::forcedBold() const
{
return d->forcedBold;
}
void QgsTextFormat::setForcedBold( bool forced )
{
d->isValid = true;
d->textFont.setBold( forced );
d->forcedBold = true;
}
bool QgsTextFormat::forcedItalic() const
{
return d->forcedItalic;
}
void QgsTextFormat::setForcedItalic( bool forced )
{
d->isValid = true;
d->textFont.setItalic( forced );
d->forcedItalic = true;
}
QStringList QgsTextFormat::families() const
{
return d->families;
}
void QgsTextFormat::setFamilies( const QStringList &families )
{
d->isValid = true;
d->families = families;
}
Qgis::RenderUnit QgsTextFormat::sizeUnit() const
{
return d->fontSizeUnits;
}
void QgsTextFormat::setSizeUnit( Qgis::RenderUnit unit )
{
d->isValid = true;
d->fontSizeUnits = unit;
}
QgsMapUnitScale QgsTextFormat::sizeMapUnitScale() const
{
return d->fontSizeMapUnitScale;
}
void QgsTextFormat::setSizeMapUnitScale( const QgsMapUnitScale &scale )
{
d->isValid = true;
d->fontSizeMapUnitScale = scale;
}
double QgsTextFormat::size() const
{
return d->fontSize;
}
void QgsTextFormat::setSize( double size )
{
d->isValid = true;
d->fontSize = size;
}
QColor QgsTextFormat::color() const
{
return d->textColor;
}
void QgsTextFormat::setColor( const QColor &color )
{
d->isValid = true;
d->textColor = color;
}
double QgsTextFormat::opacity() const
{
return d->opacity;
}
void QgsTextFormat::setOpacity( double opacity )
{
d->isValid = true;
d->opacity = opacity;
}
int QgsTextFormat::stretchFactor() const
{
return d->textFont.stretch() > 0 ? d->textFont.stretch() : 100;
}
void QgsTextFormat::setStretchFactor( int factor )
{
d->isValid = true;
d->textFont.setStretch( factor );
}
QPainter::CompositionMode QgsTextFormat::blendMode() const
{
return d->blendMode;
}
void QgsTextFormat::setBlendMode( QPainter::CompositionMode mode )
{
d->isValid = true;
d->blendMode = mode;
}
double QgsTextFormat::lineHeight() const
{
return d->multilineHeight;
}
void QgsTextFormat::setLineHeight( double height )
{
d->isValid = true;
d->multilineHeight = height;
}
Qgis::RenderUnit QgsTextFormat::lineHeightUnit() const
{
return d->multilineHeightUnits;
}
void QgsTextFormat::setLineHeightUnit( Qgis::RenderUnit unit )
{
d->isValid = true;
d->multilineHeightUnits = unit;
}
Qgis::TextOrientation QgsTextFormat::orientation() const
{
return d->orientation;
}
void QgsTextFormat::setOrientation( Qgis::TextOrientation orientation )
{
d->isValid = true;
d->orientation = orientation;
}
Qgis::Capitalization QgsTextFormat::capitalization() const
{
// bit of complexity here to maintain API..
return d->capitalization == Qgis::Capitalization::MixedCase && d->textFont.capitalization() != QFont::MixedCase
? static_cast< Qgis::Capitalization >( d->textFont.capitalization() )
: d->capitalization ;
}
void QgsTextFormat::setCapitalization( Qgis::Capitalization capitalization )
{
d->isValid = true;
d->capitalization = capitalization;
#if defined(HAS_KDE_QT5_SMALL_CAPS_FIX) || QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
d->textFont.setCapitalization( capitalization == Qgis::Capitalization::SmallCaps || capitalization == Qgis::Capitalization::AllSmallCaps ? QFont::SmallCaps : QFont::MixedCase );
#else
d->textFont.setCapitalization( QFont::MixedCase );
#endif
}
bool QgsTextFormat::allowHtmlFormatting() const
{
return d->allowHtmlFormatting;
}
void QgsTextFormat::setAllowHtmlFormatting( bool allow )
{
d->isValid = true;
d->allowHtmlFormatting = allow;
}
QColor QgsTextFormat::previewBackgroundColor() const
{
return d->previewBackgroundColor;
}
void QgsTextFormat::setPreviewBackgroundColor( const QColor &color )
{
d->isValid = true;
d->previewBackgroundColor = color;
}
void QgsTextFormat::readFromLayer( QgsVectorLayer *layer )
{
d->isValid = true;
QFont appFont = QApplication::font();
mTextFontFamily = QgsApplication::fontManager()->processFontFamilyName( layer->customProperty( QStringLiteral( "labeling/fontFamily" ), QVariant( appFont.family() ) ).toString() );
QString fontFamily = mTextFontFamily;
if ( mTextFontFamily != appFont.family() && !QgsFontUtils::fontFamilyMatchOnSystem( mTextFontFamily ) )
{
// trigger to notify about font family substitution
mTextFontFound = false;
// TODO: update when pref for how to resolve missing family (use matching algorithm or just default font) is implemented
// currently only defaults to matching algorithm for resolving [foundry], if a font of similar family is found (default for QFont)
// for now, do not use matching algorithm for substitution if family not found, substitute default instead
fontFamily = appFont.family();
}
else
{
mTextFontFound = true;
}
if ( !layer->customProperty( QStringLiteral( "labeling/fontSize" ) ).isValid() )
{
d->fontSize = appFont.pointSizeF();
}
else
{
d->fontSize = layer->customProperty( QStringLiteral( "labeling/fontSize" ) ).toDouble();
}
if ( layer->customProperty( QStringLiteral( "labeling/fontSizeUnit" ) ).toString().isEmpty() )
{
d->fontSizeUnits = layer->customProperty( QStringLiteral( "labeling/fontSizeInMapUnits" ), QVariant( false ) ).toBool() ?
Qgis::RenderUnit::MapUnits : Qgis::RenderUnit::Points;
}
else
{
bool ok = false;
d->fontSizeUnits = QgsUnitTypes::decodeRenderUnit( layer->customProperty( QStringLiteral( "labeling/fontSizeUnit" ) ).toString(), &ok );
if ( !ok )
d->fontSizeUnits = Qgis::RenderUnit::Points;
}
if ( layer->customProperty( QStringLiteral( "labeling/fontSizeMapUnitScale" ) ).toString().isEmpty() )
{
//fallback to older property
double oldMin = layer->customProperty( QStringLiteral( "labeling/fontSizeMapUnitMinScale" ), 0.0 ).toDouble();
d->fontSizeMapUnitScale.minScale = oldMin != 0 ? 1.0 / oldMin : 0;
double oldMax = layer->customProperty( QStringLiteral( "labeling/fontSizeMapUnitMaxScale" ), 0.0 ).toDouble();
d->fontSizeMapUnitScale.maxScale = oldMax != 0 ? 1.0 / oldMax : 0;
}
else
{
d->fontSizeMapUnitScale = QgsSymbolLayerUtils::decodeMapUnitScale( layer->customProperty( QStringLiteral( "labeling/fontSizeMapUnitScale" ) ).toString() );
}
int fontWeight = layer->customProperty( QStringLiteral( "labeling/fontWeight" ) ).toInt();
bool fontItalic = layer->customProperty( QStringLiteral( "labeling/fontItalic" ) ).toBool();
d->textFont = QFont( fontFamily, d->fontSize, fontWeight, fontItalic );
d->textNamedStyle = QgsFontUtils::translateNamedStyle( layer->customProperty( QStringLiteral( "labeling/namedStyle" ), QVariant( "" ) ).toString() );
QgsFontUtils::updateFontViaStyle( d->textFont, d->textNamedStyle ); // must come after textFont.setPointSizeF()
d->capitalization = static_cast< Qgis::Capitalization >( layer->customProperty( QStringLiteral( "labeling/fontCapitals" ), QVariant( 0 ) ).toUInt() );
d->textFont.setUnderline( layer->customProperty( QStringLiteral( "labeling/fontUnderline" ) ).toBool() );
d->textFont.setStrikeOut( layer->customProperty( QStringLiteral( "labeling/fontStrikeout" ) ).toBool() );
d->textFont.setLetterSpacing( QFont::AbsoluteSpacing, layer->customProperty( QStringLiteral( "labeling/fontLetterSpacing" ), QVariant( 0.0 ) ).toDouble() );
d->textFont.setWordSpacing( layer->customProperty( QStringLiteral( "labeling/fontWordSpacing" ), QVariant( 0.0 ) ).toDouble() );
d->textColor = QgsTextRendererUtils::readColor( layer, QStringLiteral( "labeling/textColor" ), Qt::black, false );
if ( layer->customProperty( QStringLiteral( "labeling/textOpacity" ) ).toString().isEmpty() )
{
d->opacity = ( 1 - layer->customProperty( QStringLiteral( "labeling/textTransp" ) ).toInt() / 100.0 ); //0 -100
}
else
{
d->opacity = ( layer->customProperty( QStringLiteral( "labeling/textOpacity" ) ).toDouble() );
}
d->blendMode = QgsPainting::getCompositionMode(
static_cast< Qgis::BlendMode >( layer->customProperty( QStringLiteral( "labeling/blendMode" ), QVariant( static_cast< int >( Qgis::BlendMode::Normal ) ) ).toUInt() ) );
d->multilineHeight = layer->customProperty( QStringLiteral( "labeling/multilineHeight" ), QVariant( 1.0 ) ).toDouble();
d->previewBackgroundColor = QgsTextRendererUtils::readColor( layer, QStringLiteral( "labeling/previewBkgrdColor" ), QColor( 255, 255, 255 ), false );
mBufferSettings.readFromLayer( layer );
mShadowSettings.readFromLayer( layer );
mBackgroundSettings.readFromLayer( layer );
}
void QgsTextFormat::readXml( const QDomElement &elem, const QgsReadWriteContext &context )
{
d->isValid = true;
QDomElement textStyleElem;
if ( elem.nodeName() == QLatin1String( "text-style" ) )
textStyleElem = elem;
else
textStyleElem = elem.firstChildElement( QStringLiteral( "text-style" ) );
QFont appFont = QApplication::font();
mTextFontFamily = QgsApplication::fontManager()->processFontFamilyName( textStyleElem.attribute( QStringLiteral( "fontFamily" ), appFont.family() ) );
QString fontFamily = mTextFontFamily;
const QDomElement familiesElem = textStyleElem.firstChildElement( QStringLiteral( "families" ) );
const QDomNodeList familyNodes = familiesElem.childNodes();
QStringList families;
families.reserve( familyNodes.size() );
for ( int i = 0; i < familyNodes.count(); ++i )
{
const QDomElement familyElem = familyNodes.at( i ).toElement();
families << familyElem.attribute( QStringLiteral( "name" ) );
}
d->families = families;
mTextFontFound = false;
QString matched;
if ( mTextFontFamily != appFont.family() && !QgsFontUtils::fontFamilyMatchOnSystem( mTextFontFamily ) )
{
if ( QgsApplication::fontManager()->tryToDownloadFontFamily( mTextFontFamily, matched ) )
{
mTextFontFound = true;
}
else
{
for ( const QString &family : std::as_const( families ) )
{
const QString processedFamily = QgsApplication::fontManager()->processFontFamilyName( family );
if ( QgsFontUtils::fontFamilyMatchOnSystem( processedFamily ) ||
QgsApplication::fontManager()->tryToDownloadFontFamily( processedFamily, matched ) )
{
mTextFontFound = true;
fontFamily = processedFamily;
break;
}
}
if ( !mTextFontFound )
{
// couldn't even find a matching font in the backup list -- substitute default instead
fontFamily = appFont.family();
}
}
}
else
{
mTextFontFound = true;
}
if ( !mTextFontFound )
{
context.pushMessage( QObject::tr( "Font “%1” not available on system" ).arg( mTextFontFamily ) );
}
if ( textStyleElem.hasAttribute( QStringLiteral( "fontSize" ) ) )
{
d->fontSize = textStyleElem.attribute( QStringLiteral( "fontSize" ) ).toDouble();
}
else
{
d->fontSize = appFont.pointSizeF();
}
if ( !textStyleElem.hasAttribute( QStringLiteral( "fontSizeUnit" ) ) )
{
d->fontSizeUnits = textStyleElem.attribute( QStringLiteral( "fontSizeInMapUnits" ) ).toUInt() == 0 ? Qgis::RenderUnit::Points
: Qgis::RenderUnit::MapUnits;
}
else
{
d->fontSizeUnits = QgsUnitTypes::decodeRenderUnit( textStyleElem.attribute( QStringLiteral( "fontSizeUnit" ) ) );
}
if ( !textStyleElem.hasAttribute( QStringLiteral( "fontSizeMapUnitScale" ) ) )
{
//fallback to older property
double oldMin = textStyleElem.attribute( QStringLiteral( "fontSizeMapUnitMinScale" ), QStringLiteral( "0" ) ).toDouble();
d->fontSizeMapUnitScale.minScale = oldMin != 0 ? 1.0 / oldMin : 0;
double oldMax = textStyleElem.attribute( QStringLiteral( "fontSizeMapUnitMaxScale" ), QStringLiteral( "0" ) ).toDouble();
d->fontSizeMapUnitScale.maxScale = oldMax != 0 ? 1.0 / oldMax : 0;
}
else
{
d->fontSizeMapUnitScale = QgsSymbolLayerUtils::decodeMapUnitScale( textStyleElem.attribute( QStringLiteral( "fontSizeMapUnitScale" ) ) );
}
int fontWeight = textStyleElem.attribute( QStringLiteral( "fontWeight" ) ).toInt();
bool fontItalic = textStyleElem.attribute( QStringLiteral( "fontItalic" ) ).toInt();
d->textFont = QFont( fontFamily, d->fontSize, fontWeight, fontItalic );
d->textFont.setPointSizeF( d->fontSize ); //double precision needed because of map units
d->textNamedStyle = QgsFontUtils::translateNamedStyle( textStyleElem.attribute( QStringLiteral( "namedStyle" ) ) );
QgsFontUtils::updateFontViaStyle( d->textFont, d->textNamedStyle ); // must come after textFont.setPointSizeF()
d->forcedBold = textStyleElem.attribute( QStringLiteral( "forcedBold" ) ).toInt();
d->forcedItalic = textStyleElem.attribute( QStringLiteral( "forcedItalic" ) ).toInt();
d->textFont.setUnderline( textStyleElem.attribute( QStringLiteral( "fontUnderline" ) ).toInt() );
d->textFont.setStrikeOut( textStyleElem.attribute( QStringLiteral( "fontStrikeout" ) ).toInt() );
d->textFont.setKerning( textStyleElem.attribute( QStringLiteral( "fontKerning" ), QStringLiteral( "1" ) ).toInt() );
d->textFont.setLetterSpacing( QFont::AbsoluteSpacing, textStyleElem.attribute( QStringLiteral( "fontLetterSpacing" ), QStringLiteral( "0" ) ).toDouble() );
d->textFont.setWordSpacing( textStyleElem.attribute( QStringLiteral( "fontWordSpacing" ), QStringLiteral( "0" ) ).toDouble() );
d->textColor = QgsSymbolLayerUtils::decodeColor( textStyleElem.attribute( QStringLiteral( "textColor" ), QgsSymbolLayerUtils::encodeColor( Qt::black ) ) );
if ( !textStyleElem.hasAttribute( QStringLiteral( "textOpacity" ) ) )
{
d->opacity = ( 1 - textStyleElem.attribute( QStringLiteral( "textTransp" ) ).toInt() / 100.0 ); //0 -100
}
else
{
d->opacity = ( textStyleElem.attribute( QStringLiteral( "textOpacity" ) ).toDouble() );
}
#ifdef HAS_KDE_QT5_FONT_STRETCH_FIX
d->textFont.setStretch( textStyleElem.attribute( QStringLiteral( "stretchFactor" ), QStringLiteral( "100" ) ).toInt() );
#endif
d->orientation = QgsTextRendererUtils::decodeTextOrientation( textStyleElem.attribute( QStringLiteral( "textOrientation" ) ) );
d->previewBackgroundColor = QgsSymbolLayerUtils::decodeColor( textStyleElem.attribute( QStringLiteral( "previewBkgrdColor" ), QgsSymbolLayerUtils::encodeColor( Qt::white ) ) );
d->blendMode = QgsPainting::getCompositionMode(
static_cast< Qgis::BlendMode >( textStyleElem.attribute( QStringLiteral( "blendMode" ), QString::number( static_cast< int >( Qgis::BlendMode::Normal ) ) ).toUInt() ) );
if ( !textStyleElem.hasAttribute( QStringLiteral( "multilineHeight" ) ) )
{
QDomElement textFormatElem = elem.firstChildElement( QStringLiteral( "text-format" ) );
d->multilineHeight = textFormatElem.attribute( QStringLiteral( "multilineHeight" ), QStringLiteral( "1" ) ).toDouble();
}
else
{
d->multilineHeight = textStyleElem.attribute( QStringLiteral( "multilineHeight" ), QStringLiteral( "1" ) ).toDouble();
}
bool ok = false;
d->multilineHeightUnits = QgsUnitTypes::decodeRenderUnit( textStyleElem.attribute( QStringLiteral( "multilineHeightUnit" ), QStringLiteral( "percent" ) ), &ok );
if ( textStyleElem.hasAttribute( QStringLiteral( "capitalization" ) ) )
d->capitalization = static_cast< Qgis::Capitalization >( textStyleElem.attribute( QStringLiteral( "capitalization" ), QString::number( static_cast< int >( Qgis::Capitalization::MixedCase ) ) ).toInt() );
else
d->capitalization = static_cast< Qgis::Capitalization >( textStyleElem.attribute( QStringLiteral( "fontCapitals" ), QStringLiteral( "0" ) ).toUInt() );
if ( d->capitalization == Qgis::Capitalization::SmallCaps || d->capitalization == Qgis::Capitalization::AllSmallCaps )
d->textFont.setCapitalization( QFont::SmallCaps );
d->allowHtmlFormatting = textStyleElem.attribute( QStringLiteral( "allowHtml" ), QStringLiteral( "0" ) ).toInt();
if ( textStyleElem.firstChildElement( QStringLiteral( "text-buffer" ) ).isNull() )
{
mBufferSettings.readXml( elem );
}
else
{
mBufferSettings.readXml( textStyleElem );
}
if ( textStyleElem.firstChildElement( QStringLiteral( "text-mask" ) ).isNull() )
{
mMaskSettings.readXml( elem );
}
else
{
mMaskSettings.readXml( textStyleElem );
}
if ( textStyleElem.firstChildElement( QStringLiteral( "shadow" ) ).isNull() )
{
mShadowSettings.readXml( elem );
}
else
{
mShadowSettings.readXml( textStyleElem );
}
if ( textStyleElem.firstChildElement( QStringLiteral( "background" ) ).isNull() )
{
mBackgroundSettings.readXml( elem, context );
}
else
{
mBackgroundSettings.readXml( textStyleElem, context );
}
QDomElement ddElem = textStyleElem.firstChildElement( QStringLiteral( "dd_properties" ) );
if ( ddElem.isNull() )
{
ddElem = elem.firstChildElement( QStringLiteral( "dd_properties" ) );
}
if ( !ddElem.isNull() )
{
d->mDataDefinedProperties.readXml( ddElem, QgsPalLayerSettings::propertyDefinitions() );
mBackgroundSettings.upgradeDataDefinedProperties( d->mDataDefinedProperties );
}
else
{
d->mDataDefinedProperties.clear();
}
}
QDomElement QgsTextFormat::writeXml( QDomDocument &doc, const QgsReadWriteContext &context ) const
{
// text style
QDomElement textStyleElem = doc.createElement( QStringLiteral( "text-style" ) );
textStyleElem.setAttribute( QStringLiteral( "fontFamily" ), d->textFont.family() );
QDomElement familiesElem = doc.createElement( QStringLiteral( "families" ) );
for ( const QString &family : std::as_const( d->families ) )
{
QDomElement familyElem = doc.createElement( QStringLiteral( "family" ) );
familyElem.setAttribute( QStringLiteral( "name" ), family );
familiesElem.appendChild( familyElem );
}
textStyleElem.appendChild( familiesElem );
textStyleElem.setAttribute( QStringLiteral( "namedStyle" ), QgsFontUtils::untranslateNamedStyle( d->textNamedStyle ) );
textStyleElem.setAttribute( QStringLiteral( "fontSize" ), d->fontSize );
textStyleElem.setAttribute( QStringLiteral( "fontSizeUnit" ), QgsUnitTypes::encodeUnit( d->fontSizeUnits ) );
textStyleElem.setAttribute( QStringLiteral( "fontSizeMapUnitScale" ), QgsSymbolLayerUtils::encodeMapUnitScale( d->fontSizeMapUnitScale ) );
textStyleElem.setAttribute( QStringLiteral( "fontWeight" ), d->textFont.weight() );
textStyleElem.setAttribute( QStringLiteral( "fontItalic" ), d->textFont.italic() );
textStyleElem.setAttribute( QStringLiteral( "fontStrikeout" ), d->textFont.strikeOut() );
textStyleElem.setAttribute( QStringLiteral( "fontUnderline" ), d->textFont.underline() );
textStyleElem.setAttribute( QStringLiteral( "forcedBold" ), d->forcedBold );
textStyleElem.setAttribute( QStringLiteral( "forcedItalic" ), d->forcedItalic );
textStyleElem.setAttribute( QStringLiteral( "textColor" ), QgsSymbolLayerUtils::encodeColor( d->textColor ) );
textStyleElem.setAttribute( QStringLiteral( "previewBkgrdColor" ), QgsSymbolLayerUtils::encodeColor( d->previewBackgroundColor ) );
textStyleElem.setAttribute( QStringLiteral( "fontLetterSpacing" ), d->textFont.letterSpacing() );
textStyleElem.setAttribute( QStringLiteral( "fontWordSpacing" ), d->textFont.wordSpacing() );
textStyleElem.setAttribute( QStringLiteral( "fontKerning" ), d->textFont.kerning() );
textStyleElem.setAttribute( QStringLiteral( "textOpacity" ), d->opacity );
#ifdef HAS_KDE_QT5_FONT_STRETCH_FIX
if ( d->textFont.stretch() > 0 )
textStyleElem.setAttribute( QStringLiteral( "stretchFactor" ), d->textFont.stretch() );
#endif
textStyleElem.setAttribute( QStringLiteral( "textOrientation" ), QgsTextRendererUtils::encodeTextOrientation( d->orientation ) );
textStyleElem.setAttribute( QStringLiteral( "blendMode" ), static_cast< int >( QgsPainting::getBlendModeEnum( d->blendMode ) ) );
textStyleElem.setAttribute( QStringLiteral( "multilineHeight" ), d->multilineHeight );
textStyleElem.setAttribute( QStringLiteral( "multilineHeightUnit" ), QgsUnitTypes::encodeUnit( d->multilineHeightUnits ) );
textStyleElem.setAttribute( QStringLiteral( "allowHtml" ), d->allowHtmlFormatting ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
textStyleElem.setAttribute( QStringLiteral( "capitalization" ), QString::number( static_cast< int >( d->capitalization ) ) );
QDomElement ddElem = doc.createElement( QStringLiteral( "dd_properties" ) );
d->mDataDefinedProperties.writeXml( ddElem, QgsPalLayerSettings::propertyDefinitions() );
textStyleElem.appendChild( mBufferSettings.writeXml( doc ) );
textStyleElem.appendChild( mMaskSettings.writeXml( doc ) );
textStyleElem.appendChild( mBackgroundSettings.writeXml( doc, context ) );
textStyleElem.appendChild( mShadowSettings.writeXml( doc ) );
textStyleElem.appendChild( ddElem );
return textStyleElem;
}
QMimeData *QgsTextFormat::toMimeData() const
{
//set both the mime color data, and the text (format settings).
QMimeData *mimeData = new QMimeData;
mimeData->setColorData( QVariant( color() ) );
QgsReadWriteContext rwContext;
QDomDocument textDoc;
QDomElement textElem = writeXml( textDoc, rwContext );
textDoc.appendChild( textElem );
mimeData->setText( textDoc.toString() );
return mimeData;
}
QgsTextFormat QgsTextFormat::fromQFont( const QFont &font )
{
QgsTextFormat format;
format.setFont( font );
if ( font.pointSizeF() > 0 )
{
format.setSize( font.pointSizeF() );
format.setSizeUnit( Qgis::RenderUnit::Points );
}
else if ( font.pixelSize() > 0 )
{
format.setSize( font.pixelSize() );
format.setSizeUnit( Qgis::RenderUnit::Pixels );
}
return format;
}
QFont QgsTextFormat::toQFont() const
{
QFont f = font();
switch ( sizeUnit() )
{
case Qgis::RenderUnit::Points:
f.setPointSizeF( size() );
break;
case Qgis::RenderUnit::Millimeters:
f.setPointSizeF( size() * 2.83464567 );
break;
case Qgis::RenderUnit::Inches:
f.setPointSizeF( size() * 72 );
break;
case Qgis::RenderUnit::Pixels:
f.setPixelSize( static_cast< int >( std::round( size() ) ) );
break;
case Qgis::RenderUnit::MapUnits:
case Qgis::RenderUnit::MetersInMapUnits:
case Qgis::RenderUnit::Unknown:
case Qgis::RenderUnit::Percentage:
// no meaning here
break;
}
return f;
}
QgsTextFormat QgsTextFormat::fromMimeData( const QMimeData *data, bool *ok )
{
if ( ok )
*ok = false;
QgsTextFormat format;
if ( !data )
return format;
QString text = data->text();
if ( !text.isEmpty() )
{
QDomDocument doc;
QDomElement elem;
QgsReadWriteContext rwContext;
if ( doc.setContent( text ) )
{
elem = doc.documentElement();
format.readXml( elem, rwContext );
if ( ok )
*ok = true;
return format;
}
}
return format;
}
bool QgsTextFormat::containsAdvancedEffects() const
{
if ( d->blendMode != QPainter::CompositionMode_SourceOver )
return true;
if ( mBufferSettings.enabled() && mBufferSettings.blendMode() != QPainter::CompositionMode_SourceOver )
return true;
if ( mBackgroundSettings.enabled() && mBackgroundSettings.blendMode() != QPainter::CompositionMode_SourceOver )
return true;
if ( mShadowSettings.enabled() && mShadowSettings.blendMode() != QPainter::CompositionMode_SourceOver )
return true;
return false;
}
QgsPropertyCollection &QgsTextFormat::dataDefinedProperties()
{
d->isValid = true;
return d->mDataDefinedProperties;
}
const QgsPropertyCollection &QgsTextFormat::dataDefinedProperties() const
{
return d->mDataDefinedProperties;
}
QSet<QString> QgsTextFormat::referencedFields( const QgsRenderContext &context ) const
{
QSet< QString > fields = d->mDataDefinedProperties.referencedFields( context.expressionContext(), true );
fields.unite( mBufferSettings.referencedFields( context ) );
fields.unite( mBackgroundSettings.referencedFields( context ) );
fields.unite( mShadowSettings.referencedFields( context ) );
fields.unite( mMaskSettings.referencedFields( context ) );
return fields;
}
void QgsTextFormat::setDataDefinedProperties( const QgsPropertyCollection &collection )
{
d->isValid = true;
d->mDataDefinedProperties = collection;
}
void QgsTextFormat::updateDataDefinedProperties( QgsRenderContext &context )
{
d->isValid = true;
if ( !d->mDataDefinedProperties.hasActiveProperties() )
return;
QString ddFontFamily;
context.expressionContext().setOriginalValueVariable( d->textFont.family() );
QVariant exprVal = d->mDataDefinedProperties.value( QgsPalLayerSettings::Family, context.expressionContext() );
if ( !QgsVariantUtils::isNull( exprVal ) )
{
QString family = exprVal.toString().trimmed();
family = QgsApplication::fontManager()->processFontFamilyName( family );
if ( d->textFont.family() != family )
{
// testing for ddFontFamily in QFontDatabase.families() may be slow to do for every feature
// (i.e. don't use QgsFontUtils::fontFamilyMatchOnSystem( family ) here)
if ( QgsFontUtils::fontFamilyOnSystem( family ) )
{
ddFontFamily = family;
}
}
}
// data defined named font style?
QString ddFontStyle;
context.expressionContext().setOriginalValueVariable( d->textNamedStyle );
exprVal = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontStyle, context.expressionContext() );
if ( !QgsVariantUtils::isNull( exprVal ) )
{
QString fontstyle = exprVal.toString().trimmed();
ddFontStyle = fontstyle;
}
bool ddBold = false;
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::Bold ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.bold() );
ddBold = d->mDataDefinedProperties.valueAsBool( QgsPalLayerSettings::Bold, context.expressionContext(), false ) ;
}
bool ddItalic = false;
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::Italic ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.italic() );
ddItalic = d->mDataDefinedProperties.valueAsBool( QgsPalLayerSettings::Italic, context.expressionContext(), false );
}
// TODO: update when pref for how to resolve missing family (use matching algorithm or just default font) is implemented
// (currently defaults to what has been read in from layer settings)
QFont newFont;
QFontDatabase fontDb;
QFont appFont = QApplication::font();
bool newFontBuilt = false;
if ( ddBold || ddItalic )
{
// new font needs built, since existing style needs removed
newFont = QFont( !ddFontFamily.isEmpty() ? ddFontFamily : d->textFont.family() );
newFontBuilt = true;
newFont.setBold( ddBold );
newFont.setItalic( ddItalic );
}
else if ( !ddFontStyle.isEmpty()
&& ddFontStyle.compare( QLatin1String( "Ignore" ), Qt::CaseInsensitive ) != 0 )
{
if ( !ddFontFamily.isEmpty() )
{
// both family and style are different, build font from database
QFont styledfont = fontDb.font( ddFontFamily, ddFontStyle, appFont.pointSize() );
if ( appFont != styledfont )
{
newFont = styledfont;
newFontBuilt = true;
}
}
// update the font face style
QgsFontUtils::updateFontViaStyle( newFontBuilt ? newFont : d->textFont, ddFontStyle );
}
else if ( !ddFontFamily.isEmpty() )
{
if ( ddFontStyle.compare( QLatin1String( "Ignore" ), Qt::CaseInsensitive ) != 0 )
{
// just family is different, build font from database
QFont styledfont = fontDb.font( ddFontFamily, d->textNamedStyle, appFont.pointSize() );
if ( appFont != styledfont )
{
newFont = styledfont;
newFontBuilt = true;
}
}
else
{
newFont = QFont( ddFontFamily );
newFontBuilt = true;
}
}
if ( newFontBuilt )
{
// copy over existing font settings
newFont.setUnderline( d->textFont.underline() );
newFont.setStrikeOut( d->textFont.strikeOut() );
newFont.setWordSpacing( d->textFont.wordSpacing() );
newFont.setLetterSpacing( QFont::AbsoluteSpacing, d->textFont.letterSpacing() );
d->textFont = newFont;
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::Underline ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.underline() );
d->textFont.setUnderline( d->mDataDefinedProperties.valueAsBool( QgsPalLayerSettings::Underline, context.expressionContext(), d->textFont.underline() ) );
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::Strikeout ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.strikeOut() );
d->textFont.setStrikeOut( d->mDataDefinedProperties.valueAsBool( QgsPalLayerSettings::Strikeout, context.expressionContext(), d->textFont.strikeOut() ) );
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::Color ) )
{
context.expressionContext().setOriginalValueVariable( QgsSymbolLayerUtils::encodeColor( d->textColor ) );
d->textColor = d->mDataDefinedProperties.valueAsColor( QgsPalLayerSettings::Color, context.expressionContext(), d->textColor );
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::Size ) )
{
context.expressionContext().setOriginalValueVariable( size() );
d->fontSize = d->mDataDefinedProperties.valueAsDouble( QgsPalLayerSettings::Size, context.expressionContext(), d->fontSize );
}
exprVal = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontSizeUnit, context.expressionContext() );
if ( !QgsVariantUtils::isNull( exprVal ) )
{
QString units = exprVal.toString();
if ( !units.isEmpty() )
{
bool ok;
Qgis::RenderUnit res = QgsUnitTypes::decodeRenderUnit( units, &ok );
if ( ok )
d->fontSizeUnits = res;
}
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::FontOpacity ) )
{
context.expressionContext().setOriginalValueVariable( d->opacity * 100 );
const QVariant val = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontOpacity, context.expressionContext(), d->opacity * 100 );
if ( !QgsVariantUtils::isNull( val ) )
{
d->opacity = val.toDouble() / 100.0;
}
}
#ifdef HAS_KDE_QT5_FONT_STRETCH_FIX
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::FontStretchFactor ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.stretch() );
const QVariant val = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontStretchFactor, context.expressionContext(), d->textFont.stretch() );
if ( !QgsVariantUtils::isNull( val ) )
{
d->textFont.setStretch( val.toInt() );
}
}
#endif
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::TextOrientation ) )
{
const QString encoded = QgsTextRendererUtils::encodeTextOrientation( d->orientation );
context.expressionContext().setOriginalValueVariable( encoded );
d->orientation = QgsTextRendererUtils::decodeTextOrientation( d->mDataDefinedProperties.value( QgsPalLayerSettings::TextOrientation, context.expressionContext(), encoded ).toString() );
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::FontLetterSpacing ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.letterSpacing() );
const QVariant val = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontLetterSpacing, context.expressionContext(), d->textFont.letterSpacing() );
if ( !QgsVariantUtils::isNull( val ) )
{
d->textFont.setLetterSpacing( QFont::AbsoluteSpacing, val.toDouble() );
}
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::FontWordSpacing ) )
{
context.expressionContext().setOriginalValueVariable( d->textFont.wordSpacing() );
const QVariant val = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontWordSpacing, context.expressionContext(), d->textFont.wordSpacing() );
if ( !QgsVariantUtils::isNull( val ) )
{
d->textFont.setWordSpacing( val.toDouble() );
}
}
if ( d->mDataDefinedProperties.isActive( QgsPalLayerSettings::FontBlendMode ) )
{
exprVal = d->mDataDefinedProperties.value( QgsPalLayerSettings::FontBlendMode, context.expressionContext() );
QString blendstr = exprVal.toString().trimmed();
if ( !blendstr.isEmpty() )
d->blendMode = QgsSymbolLayerUtils::decodeBlendMode( blendstr );
}
mShadowSettings.updateDataDefinedProperties( context, d->mDataDefinedProperties );
mBackgroundSettings.updateDataDefinedProperties( context, d->mDataDefinedProperties );
mBufferSettings.updateDataDefinedProperties( context, d->mDataDefinedProperties );
mMaskSettings.updateDataDefinedProperties( context, d->mDataDefinedProperties );
}
QPixmap QgsTextFormat::textFormatPreviewPixmap( const QgsTextFormat &format, QSize size, const QString &previewText, int padding, double devicePixelRatio )
{
QgsTextFormat tempFormat = format;
QPixmap pixmap( size * devicePixelRatio );
pixmap.fill( Qt::transparent );
pixmap.setDevicePixelRatio( devicePixelRatio );
QPainter painter;
painter.begin( &pixmap );
painter.setRenderHint( QPainter::Antialiasing );
const QRectF rect( 0, 0, size.width(), size.height() );
// shameless eye candy - use a subtle gradient when drawing background
painter.setPen( Qt::NoPen );
QColor background1 = tempFormat.previewBackgroundColor();
if ( ( background1.lightnessF() < 0.7 ) )
{
background1 = background1.darker( 125 );
}
else
{
background1 = background1.lighter( 125 );
}
QColor background2 = tempFormat.previewBackgroundColor();
QLinearGradient linearGrad( QPointF( 0, 0 ), QPointF( 0, rect.height() ) );
linearGrad.setColorAt( 0, background1 );
linearGrad.setColorAt( 1, background2 );
painter.setBrush( QBrush( linearGrad ) );
if ( size.width() > 30 )
{
painter.drawRoundedRect( rect, 6, 6 );
}
else
{
// don't use rounded rect for small previews
painter.drawRect( rect );
}
painter.setBrush( Qt::NoBrush );
painter.setPen( Qt::NoPen );
padding += 1; // move text away from background border
QgsRenderContext context;
QgsMapToPixel newCoordXForm;
newCoordXForm.setParameters( 1, 0, 0, 0, 0, 0 );
context.setMapToPixel( newCoordXForm );
QWidget *activeWindow = QApplication::activeWindow();
const double physicalDpiX = ( activeWindow && activeWindow->screen() ? activeWindow->screen()->physicalDotsPerInch() : 96.0 );
context.setScaleFactor( physicalDpiX / 25.4 );
context.setDevicePixelRatio( devicePixelRatio );
context.setUseAdvancedEffects( true );
context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );
context.setPainter( &painter );
context.setFlag( Qgis::RenderContextFlag::Antialiasing, true );
// slightly inset text to account for buffer/background
const double fontSize = context.convertToPainterUnits( tempFormat.size(), tempFormat.sizeUnit(), tempFormat.sizeMapUnitScale() );
double xtrans = 0;
if ( tempFormat.buffer().enabled() )
xtrans = tempFormat.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
? fontSize * tempFormat.buffer().size() / 100
: context.convertToPainterUnits( tempFormat.buffer().size(), tempFormat.buffer().sizeUnit(), tempFormat.buffer().sizeMapUnitScale() );
if ( tempFormat.background().enabled() && tempFormat.background().sizeType() != QgsTextBackgroundSettings::SizeFixed )
xtrans = std::max( xtrans, context.convertToPainterUnits( tempFormat.background().size().width(), tempFormat.background().sizeUnit(), tempFormat.background().sizeMapUnitScale() ) );
double ytrans = 0.0;
if ( tempFormat.buffer().enabled() )
ytrans = std::max( ytrans, tempFormat.buffer().sizeUnit() == Qgis::RenderUnit::Percentage
? fontSize * tempFormat.buffer().size() / 100
: context.convertToPainterUnits( tempFormat.buffer().size(), tempFormat.buffer().sizeUnit(), tempFormat.buffer().sizeMapUnitScale() ) );
if ( tempFormat.background().enabled() )
ytrans = std::max( ytrans, context.convertToPainterUnits( tempFormat.background().size().height(), tempFormat.background().sizeUnit(), tempFormat.background().sizeMapUnitScale() ) );
const QStringList text = QStringList() << ( previewText.isEmpty() ? QObject::tr( "Aa" ) : previewText );
const double textHeight = QgsTextRenderer::textHeight( context, tempFormat, text, Qgis::TextLayoutMode::Rectangle );
QRectF textRect = rect;
textRect.setLeft( xtrans + padding );
textRect.setWidth( rect.width() - xtrans - 2 * padding );
if ( textRect.width() > 2000 )
textRect.setWidth( 2000 - 2 * padding );
const double bottom = textRect.height() / 2 + textHeight / 2;
textRect.setTop( bottom - textHeight );
textRect.setBottom( bottom );
QgsTextRenderer::drawText( textRect, 0, Qgis::TextHorizontalAlignment::Center, text, context, tempFormat );
// draw border on top of text
painter.setBrush( Qt::NoBrush );
painter.setPen( QPen( tempFormat.previewBackgroundColor().darker( 150 ), 0 ) );
if ( size.width() > 30 )
{
painter.drawRoundedRect( rect, 6, 6 );
}
else
{
// don't use rounded rect for small previews
painter.drawRect( rect );
}
painter.end();
return pixmap;
}