Skip to content

Commit

Permalink
[feature] Support img tags in HTML label text
Browse files Browse the repository at this point in the history
Allows use of img tags in HTML label content. The following logic
is applied:

- Image path is set via the src="xxx" attribute. Local, HTTP, and
base64 encoded paths are permitted
- Any image format readable by QGIS can be used
- Image sizes can be specified via the width="##" and height="##"
attributes. If width or height is not specified it will automatically
be calculated from the original image size
- If width or height are specified, they are considered to be
in POINTS
- The css width/height settings are NOT respected (this is a Qt
limitation)
- Images are not supported for curved text labels
- Images are placed inline only, floating images are not supported

Sponsored by City of Freiburg im Breisgau
  • Loading branch information
nyalldawson committed Sep 13, 2024
1 parent c33c39b commit bae0d40
Show file tree
Hide file tree
Showing 18 changed files with 434 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,62 @@ Returns whether the format has overline enabled.
Sets whether the format has overline ``enabled``.

.. seealso:: :py:func:`overline`
%End

QString imagePath() const;
%Docstring
Returns the path to the image to render, if the format applies to a document image fragment.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`imageSize`

.. seealso:: :py:func:`setImagePath`

.. versionadded:: 3.40
%End

void setImagePath( const QString &path );
%Docstring
Sets the ``path`` to the image to render, if the format applies to a document image fragment.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`setImageSize`

.. seealso:: :py:func:`imagePath`

.. versionadded:: 3.40
%End

QSizeF imageSize() const;
%Docstring
Returns the image size, if the format applies to a document image fragment.

The image size is always considered to be in :py:class:`Qgis`.RenderUnit.Points.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`imagePath`

.. seealso:: :py:func:`setImageSize`

.. versionadded:: 3.40
%End

void setImageSize( const QSizeF &size );
%Docstring
Sets the image ``size``, if the format applies to a document image fragment.

The image size is always considered to be in :py:class:`Qgis`.RenderUnit.Points.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`setImagePath`

.. seealso:: :py:func:`imageSize`

.. versionadded:: 3.40
%End

bool hasVerticalAlignmentSet() const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ Returns the vertical offset from a text block's baseline which should be applied
to the fragment at the specified index within that block.

.. versionadded:: 3.30
%End

double fragmentFixedHeight( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the fixed height of the fragment at the specified block and fragment index, or -1 if the fragment does not have a fixed height.

.. versionadded:: 3.40
%End

double verticalOrientationXOffset( int blockIndex ) const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
class QgsTextFragment
{
%Docstring(signature="appended")
Stores a fragment of text along with formatting overrides to be used when rendering the fragment.
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.

Text fragments consist of either a block of text or another atomic component of a document (such as an image).

Each fragment has an associated :py:func:`~characterFormat`, which specifies the text formatting overrides
to use when rendering the fragment. Additionally, the :py:func:`~characterFormat` may contain properties
for other fragment types, such as image paths and sizes for image fragments.

.. warning::

Expand Down Expand Up @@ -77,6 +83,13 @@ Returns the character formatting for the fragment.
Sets the character ``format`` for the fragment.

.. seealso:: :py:func:`characterFormat`
%End

bool isImage() const;
%Docstring
Returns ``True`` if the fragment represents an image.

.. versionadded:: 3.40
%End

double horizontalAdvance( const QFont &font, const QgsRenderContext &context, bool fontHasBeenUpdatedForFragment = false, double scaleFactor = 1.0 ) const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,62 @@ Returns whether the format has overline enabled.
Sets whether the format has overline ``enabled``.

.. seealso:: :py:func:`overline`
%End

QString imagePath() const;
%Docstring
Returns the path to the image to render, if the format applies to a document image fragment.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`imageSize`

.. seealso:: :py:func:`setImagePath`

.. versionadded:: 3.40
%End

void setImagePath( const QString &path );
%Docstring
Sets the ``path`` to the image to render, if the format applies to a document image fragment.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`setImageSize`

.. seealso:: :py:func:`imagePath`

.. versionadded:: 3.40
%End

QSizeF imageSize() const;
%Docstring
Returns the image size, if the format applies to a document image fragment.

The image size is always considered to be in :py:class:`Qgis`.RenderUnit.Points.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`imagePath`

.. seealso:: :py:func:`setImageSize`

.. versionadded:: 3.40
%End

void setImageSize( const QSizeF &size );
%Docstring
Sets the image ``size``, if the format applies to a document image fragment.

The image size is always considered to be in :py:class:`Qgis`.RenderUnit.Points.

.. seealso:: :py:func:`QgsTextFragment.isImage`

.. seealso:: :py:func:`setImagePath`

.. seealso:: :py:func:`imageSize`

.. versionadded:: 3.40
%End

bool hasVerticalAlignmentSet() const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ Returns the vertical offset from a text block's baseline which should be applied
to the fragment at the specified index within that block.

.. versionadded:: 3.30
%End

double fragmentFixedHeight( int blockIndex, int fragmentIndex, Qgis::TextLayoutMode mode ) const;
%Docstring
Returns the fixed height of the fragment at the specified block and fragment index, or -1 if the fragment does not have a fixed height.

.. versionadded:: 3.40
%End

double verticalOrientationXOffset( int blockIndex ) const;
Expand Down
15 changes: 14 additions & 1 deletion python/core/auto_generated/textrenderer/qgstextfragment.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@
class QgsTextFragment
{
%Docstring(signature="appended")
Stores a fragment of text along with formatting overrides to be used when rendering the fragment.
Stores a fragment of document along with formatting overrides to be used when rendering the fragment.

Text fragments consist of either a block of text or another atomic component of a document (such as an image).

Each fragment has an associated :py:func:`~characterFormat`, which specifies the text formatting overrides
to use when rendering the fragment. Additionally, the :py:func:`~characterFormat` may contain properties
for other fragment types, such as image paths and sizes for image fragments.

.. warning::

Expand Down Expand Up @@ -77,6 +83,13 @@ Returns the character formatting for the fragment.
Sets the character ``format`` for the fragment.

.. seealso:: :py:func:`characterFormat`
%End

bool isImage() const;
%Docstring
Returns ``True`` if the fragment represents an image.

.. versionadded:: 3.40
%End

double horizontalAdvance( const QFont &font, const QgsRenderContext &context, bool fontHasBeenUpdatedForFragment = false, double scaleFactor = 1.0 ) const;
Expand Down
11 changes: 11 additions & 0 deletions src/core/qgsabstractcontentcache_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

#include "qgsabstractcontentcache.h"
#include "qgssetrequestinitiator_p.h"
#include <QRegularExpression>

template<class T>
QByteArray QgsAbstractContentCache<T>::getContent( const QString &path, const QByteArray &missingContent, const QByteArray &fetchingContent, bool blocking ) const
Expand All @@ -44,6 +45,16 @@ QByteArray QgsAbstractContentCache<T>::getContent( const QString &path, const QB
const QByteArray base64 = path.mid( 7 ).toLocal8Bit(); // strip 'base64:' prefix
return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
}
else
{
const thread_local QRegularExpression rx( QStringLiteral( "^data:image/.*;base64,(.*)$" ) );
const QRegularExpressionMatch base64Match = rx.match( path );
if ( base64Match.hasMatch() )
{
const QByteArray base64 = base64Match.captured( 1 ).toLocal8Bit(); // strip prefix
return QByteArray::fromBase64( base64, QByteArray::OmitTrailingEquals );
}
}

// maybe it's a url...
if ( !path.contains( QLatin1String( "://" ) ) ) // otherwise short, relative SVG paths might be considered URLs
Expand Down
22 changes: 19 additions & 3 deletions src/core/qgsimagecache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ QImage QgsImageCache::pathAsImagePrivate( const QString &f, const QSize size, co

fitsInCache = true;

const thread_local QRegularExpression rx( QStringLiteral( "^data:image/.*;base64,(.*)$" ), QRegularExpression::DotMatchesEverythingOption );
const QRegularExpressionMatch base64Match = rx.match( file );
if ( base64Match.hasMatch() )
{
file = QStringLiteral( "base64:%1" ).arg( base64Match.captured( 1 ) );
}

QgsImageCacheEntry *currentEntry = findExistingEntry( new QgsImageCacheEntry( file, size, keepAspectRatio, opacity, targetDpi, frameNumber ) );

QImage result;
Expand Down Expand Up @@ -206,13 +213,22 @@ QImage QgsImageCache::pathAsImagePrivate( const QString &f, const QSize size, co
return result;
}

bool isBase64Path( const QString &path )
{
if ( path.startsWith( QLatin1String( "base64:" ) ) )
return true;

const thread_local QRegularExpression rx( QStringLiteral( "^data:image/.*;base64," ), QRegularExpression::DotMatchesEverythingOption );
return rx.match( path ).hasMatch();
}

QSize QgsImageCache::originalSize( const QString &path, bool blocking ) const
{
if ( path.isEmpty() )
return QSize();

// direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
if ( !isBase64Path( path ) && QFile::exists( path ) )
{
const QImageReader reader( path );
if ( reader.size().isValid() )
Expand Down Expand Up @@ -298,7 +314,7 @@ void QgsImageCache::prepareAnimation( const QString &path )
std::unique_ptr< QImageReader > reader;
std::unique_ptr< QBuffer > buffer;

if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
if ( !isBase64Path( path ) && QFile::exists( path ) )
{
const QString basePart = QFileInfo( path ).baseName();
int id = 1;
Expand Down Expand Up @@ -355,7 +371,7 @@ QImage QgsImageCache::renderImage( const QString &path, QSize size, const bool k
isBroken = false;

// direct read if path is a file -- maybe more efficient than going the bytearray route? (untested!)
if ( !path.startsWith( QLatin1String( "base64:" ) ) && QFile::exists( path ) )
if ( !isBase64Path( path ) && QFile::exists( path ) )
{
QImageReader reader( path );
reader.setAutoTransform( true );
Expand Down
26 changes: 26 additions & 0 deletions src/core/textrenderer/qgstextcharacterformat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ QgsTextCharacterFormat::QgsTextCharacterFormat( const QTextCharFormat &format )
if ( !families.isEmpty() )
mFontFamily = families.at( 0 );
}
if ( format.isImageFormat() )
{
const QTextImageFormat imageFormat = format.toImageFormat();
mImagePath = imageFormat.name();
mImageSize = QSizeF( imageFormat.width(), imageFormat.height() );
}
}

void QgsTextCharacterFormat::overrideWith( const QgsTextCharacterFormat &other )
Expand Down Expand Up @@ -168,6 +174,26 @@ void QgsTextCharacterFormat::setOverline( QgsTextCharacterFormat::BooleanValue e
mOverline = enabled;
}

QString QgsTextCharacterFormat::imagePath() const
{
return mImagePath;
}

void QgsTextCharacterFormat::setImagePath( const QString &path )
{
mImagePath = path;
}

QSizeF QgsTextCharacterFormat::imageSize() const
{
return mImageSize;
}

void QgsTextCharacterFormat::setImageSize( const QSizeF &size )
{
mImageSize = size;
}

void QgsTextCharacterFormat::updateFontForFormat( QFont &font, const QgsRenderContext &context, const double scaleFactor ) const
{
// important -- MUST set family first
Expand Down
Loading

0 comments on commit bae0d40

Please sign in to comment.