QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
qgsfonttextureatlasgenerator.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsfonttextureatlasgenerator.cpp
3 --------------------------------------
4 Date : September 2025
5 Copyright : (C) 2025 by Nyall Dawson
6 Email : nyall dot dawson at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgscolorrampimpl.h"
19#include "qgspallabeling.h"
20#include "qgsrendercontext.h"
21#include "qgstextformat.h"
22#include "qgstextrenderer.h"
23
24#include <QPainter>
25#include <QRegularExpressionMatch>
26#include <QSet>
27#include <QString>
28
29using namespace Qt::StringLiterals;
30
31// rectpack2D library
32#include <finders_interface.h>
33
35
36class QgsCharTextureRect
37{
38 public:
39 QgsCharTextureRect( const QString &grapheme, const QSize &boundingRectSize, const QPoint &characterOffsetFromOrigin, int texturePaddingPixels )
40 : grapheme( grapheme )
41 , boundingRectSize( boundingRectSize )
42 , characterOffsetFromOrigin( characterOffsetFromOrigin )
43 {
44 paddedRect = rectpack2D::rect_xywh( 0, 0, boundingRectSize.width() + 2 * texturePaddingPixels, boundingRectSize.height() + 2 * texturePaddingPixels );
45 }
46
47 // get_rect must be implemented for rectpack2D compatibility:
48 auto &get_rect()
49 {
50 return paddedRect;
51 }
52 const auto &get_rect() const
53 {
54 return paddedRect;
55 }
56
57 QRect asQRect() const
58 {
59 return QRect( paddedRect.x, paddedRect.y, paddedRect.w, paddedRect.h );
60 }
61
62 QString grapheme;
64 QSize boundingRectSize;
66 QPoint characterOffsetFromOrigin;
68 rectpack2D::rect_xywh paddedRect;
69};
70
72
73
78
80{
81 return static_cast< int >( mRects.size() );
82}
83
84QRect QgsFontTextureAtlas::rect( const QString &grapheme ) const
85{
86 auto it = mGraphemeIndices.constFind( grapheme );
87 if ( it == mGraphemeIndices.constEnd() )
88 return QRect();
89
90 return mRects[it.value()].asQRect();
91}
92
93int QgsFontTextureAtlas::graphemeCount( const QString &string ) const
94{
95 auto it = mStringMetrics.constFind( string );
96 if ( it == mStringMetrics.constEnd() )
97 return 0;
98
99 return it->graphemeMetrics.count();
100}
101
102int QgsFontTextureAtlas::totalWidth( const QString &string ) const
103{
104 auto it = mStringMetrics.constFind( string );
105 if ( it == mStringMetrics.constEnd() )
106 return 0;
107
108 return it->totalWidth;
109}
110
111QPoint QgsFontTextureAtlas::pixelOffsetForGrapheme( const QString &string, int graphemeIndex ) const
112{
113 auto it = mStringMetrics.constFind( string );
114 if ( it == mStringMetrics.constEnd() )
115 return QPoint();
116
117 const GraphemeMetric &graphemeMetrics = it.value().graphemeMetrics[graphemeIndex];
118 auto charIt = mGraphemeIndices.constFind( graphemeMetrics.grapheme );
119 if ( charIt == mGraphemeIndices.constEnd() )
120 return QPoint();
121
122 return QPoint( graphemeMetrics.horizontalAdvance, -( mRects[charIt.value()].boundingRectSize.height() + mRects[charIt.value()].characterOffsetFromOrigin.y() ) );
123}
124
125QRect QgsFontTextureAtlas::textureRectForGrapheme( const QString &string, int graphemeIndex ) const
126{
127 auto it = mStringMetrics.constFind( string );
128 if ( it == mStringMetrics.constEnd() )
129 return QRect();
130
131 const GraphemeMetric &graphemeMetrics = it.value().graphemeMetrics.value( graphemeIndex );
132 auto charIt = mGraphemeIndices.constFind( graphemeMetrics.grapheme );
133 if ( charIt == mGraphemeIndices.constEnd() )
134 return QRect();
135
136 return mRects[charIt.value()].asQRect();
137}
138
140{
141 if ( mAtlasSize.isEmpty() )
142 return QImage();
143
144 QImage res( mAtlasSize, QImage::Format_ARGB32_Premultiplied );
145 res.fill( Qt::transparent );
146
147 QPainter painter( &res );
149 for ( const QgsCharTextureRect &rect : mRects )
150 {
151 QgsTextRenderer::drawText( QPointF( -rect.characterOffsetFromOrigin.x() + rect.paddedRect.x + mTexturePaddingPixels, -rect.characterOffsetFromOrigin.y() + rect.paddedRect.y + mTexturePaddingPixels ), 0, Qgis::TextHorizontalAlignment::Left, { rect.grapheme }, context, mFormat );
152 }
153 painter.end();
154
155 return res;
156}
157
159{
160 if ( mAtlasSize.isEmpty() )
161 return QImage();
162
163 QImage res( mAtlasSize, QImage::Format_ARGB32_Premultiplied );
164 res.fill( Qt::transparent );
165
166 QPainter painter( &res );
167 painter.setPen( Qt::NoPen );
169 ramp.setTotalColorCount( static_cast< int >( mRects.size() ) );
170 double index = 0;
171 for ( const QgsCharTextureRect &rect : mRects )
172 {
173 const QColor color = ramp.color( index / ( static_cast< int >( mRects.size() ) - 1 ) );
174 index += 1;
175 painter.setBrush( QBrush( color ) );
176 painter.drawRect( rect.asQRect() );
177 }
178 painter.end();
179
180 return res;
181}
182
183
184//
185// QgsFontTextureAtlasGenerator
186//
187
189{
190 QgsRenderContext context;
191 context.setScaleFactor( 96.0 / 25.4 );
192 const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, format );
193
194 int texturePaddingPixels = 2;
195 if ( format.buffer().enabled() )
196 {
197 texturePaddingPixels += static_cast< int >( std::ceil( context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit() ) ) );
198 }
199
200 // collect unique graphemes from all strings
201 QSet<QString> uniqueGraphemes;
202 QMap< QString, QgsFontTextureAtlas::StringMetrics > stringMetrics;
203 for ( const QString &string : strings )
204 {
205 const QStringList graphemes = QgsPalLabeling::splitToGraphemes( string );
206
207 QgsFontTextureAtlas::StringMetrics thisStringMetrics;
208 thisStringMetrics.totalWidth = fontMetrics.boundingRect( string ).width();
209 thisStringMetrics.graphemeMetrics.reserve( graphemes.size() );
210 QString currentString;
211 for ( const QString &grapheme : graphemes )
212 {
213 uniqueGraphemes.insert( grapheme );
214 thisStringMetrics.graphemeMetrics << QgsFontTextureAtlas::GraphemeMetric( static_cast< int >( std::round( fontMetrics.horizontalAdvance( currentString ) ) ), grapheme );
215 currentString += grapheme;
216 }
217 stringMetrics.insert( string, thisStringMetrics );
218 }
219
220 if ( uniqueGraphemes.isEmpty() )
221 {
222 return QgsFontTextureAtlas();
223 }
224
225 // get bounding rectangles for all the unique characters we need to render
226 std::vector<QgsCharTextureRect> charRects;
227 charRects.reserve( uniqueGraphemes.size() );
228 for ( const QString &c : uniqueGraphemes )
229 {
230 const thread_local QRegularExpression sWhitespaceRx( u"^\\s+$"_s );
231 if ( sWhitespaceRx.match( c ).hasMatch() )
232 continue;
233
234 const QRect boundingRect = c.size() == 1 ? fontMetrics.boundingRect( c.at( 0 ) ).toRect() : fontMetrics.boundingRect( c ).toRect();
235 charRects.emplace_back( QgsCharTextureRect( c, boundingRect.size(), boundingRect.topLeft(), texturePaddingPixels ) );
236 }
237
238 // pack character rects into an atlas
239 using spacesType = rectpack2D::empty_spaces<false, rectpack2D::default_empty_spaces>;
240
241 bool result = true;
242 auto reportSuccessful = []( rectpack2D::rect_xywh & ) {
243 return rectpack2D::callback_result::CONTINUE_PACKING;
244 };
245
246 auto reportUnsuccessful = [&result]( rectpack2D::rect_xywh & ) {
247 result = false;
248 return rectpack2D::callback_result::ABORT_PACKING;
249 };
250
251 const auto discardStep = -4;
252
253 auto byWidth = []( const rectpack2D::rect_xywh *a, const rectpack2D::rect_xywh *b ) {
254 return a->w > b->w;
255 };
256
257 const rectpack2D::rect_wh resultSize = rectpack2D::find_best_packing<spacesType>(
258 charRects,
259 rectpack2D::make_finder_input(
260 1024,
261 discardStep,
262 reportSuccessful,
263 reportUnsuccessful,
264 rectpack2D::flipping_option::DISABLED
265 ),
266 byWidth
267 );
268
269 if ( !result )
270 return QgsFontTextureAtlas();
271
273 res.mFormat = format;
274 res.mRects = std::move( charRects );
275 res.mAtlasSize = QSize( resultSize.w, resultSize.h );
276 res.mStringMetrics = std::move( stringMetrics );
277 res.mTexturePaddingPixels = texturePaddingPixels;
278
279 int index = 0;
280 for ( const QgsCharTextureRect &r : res.mRects )
281 {
282 res.mGraphemeIndices.insert( r.grapheme, index++ );
283 }
284
285 return res;
286}
static QgsFontTextureAtlas create(const QgsTextFormat &format, const QStringList &strings)
Creates the texture atlas for a set of strings, using the specified text format.
Encapsulates a font texture atlas.
int graphemeCount(const QString &string) const
Returns the number of graphemes to render for a given string.
QImage renderAtlasTexture() const
Renders the combined texture atlas, containing all required characters.
int totalWidth(const QString &string) const
Returns the total width (in pixels) required for a given string.
QRect textureRectForGrapheme(const QString &string, int graphemeIndex) const
Returns the packed rectangle for the texture for the matching grapheme.
QRect rect(const QString &grapheme) const
Returns the packed rectangle for the texture for the specified grapheme.
QgsFontTextureAtlas & operator=(const QgsFontTextureAtlas &other)
QImage renderDebugTexture() const
Renders a debug texture.
QPoint pixelOffsetForGrapheme(const QString &string, int graphemeIndex) const
Returns the pixel offset at which the texture for the matching grapheme should be placed.
int count() const
Returns the number of textures in the atlas.
static QStringList splitToGraphemes(const QString &text)
Splits a text string to a list of graphemes, which are the smallest allowable character divisions in ...
A color ramp consisting of random colors, constrained within component ranges.
virtual void setTotalColorCount(int colorCount)
Sets the desired total number of unique colors for the resultant ramp.
QColor color(double value) const override
Returns the color corresponding to a specified value.
Contains information about the context of a rendering operation.
void setScaleFactor(double factor)
Sets the scaling factor for the render to convert painter units to physical sizes.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
static QgsRenderContext fromQPainter(QPainter *painter)
Creates a default render context given a pixel based QPainter destination.
Qgis::RenderUnit sizeUnit() const
Returns the units for the buffer size.
double size() const
Returns the size of the buffer.
bool enabled() const
Returns whether the buffer is enabled.
Container for all settings relating to text rendering.
QgsTextBufferSettings & buffer()
Returns a reference to the text buffer settings.
static void drawText(const QRectF &rect, double rotation, Qgis::TextHorizontalAlignment alignment, const QStringList &textLines, QgsRenderContext &context, const QgsTextFormat &format, bool drawAsOutlines=true, Qgis::TextVerticalAlignment vAlignment=Qgis::TextVerticalAlignment::Top, Qgis::TextRendererFlags flags=Qgis::TextRendererFlags(), Qgis::TextLayoutMode mode=Qgis::TextLayoutMode::Rectangle)
Draws text within a rectangle using the specified settings.
static QFontMetricsF fontMetrics(QgsRenderContext &context, const QgsTextFormat &format, double scaleFactor=1.0)
Returns the font metrics for the given text format, when rendered in the specified render context.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c