QGIS API Documentation 3.99.0-Master (26c88405ac0)
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
28// rectpack2D library
29#include <finders_interface.h>
30
32
33class QgsCharTextureRect
34{
35 public:
36 QgsCharTextureRect( const QString &grapheme, const QSize &boundingRectSize, const QPoint &characterOffsetFromOrigin, int texturePaddingPixels )
37 : grapheme( grapheme )
38 , boundingRectSize( boundingRectSize )
39 , characterOffsetFromOrigin( characterOffsetFromOrigin )
40 {
41 paddedRect = rectpack2D::rect_xywh( 0, 0, boundingRectSize.width() + 2 * texturePaddingPixels, boundingRectSize.height() + 2 * texturePaddingPixels );
42 }
43
44 // get_rect must be implemented for rectpack2D compatibility:
45 auto &get_rect()
46 {
47 return paddedRect;
48 }
49 const auto &get_rect() const
50 {
51 return paddedRect;
52 }
53
54 QRect asQRect() const
55 {
56 return QRect( paddedRect.x, paddedRect.y, paddedRect.w, paddedRect.h );
57 }
58
59 QString grapheme;
61 QSize boundingRectSize;
63 QPoint characterOffsetFromOrigin;
65 rectpack2D::rect_xywh paddedRect;
66};
67
69
70
75
77{
78 return static_cast< int >( mRects.size() );
79}
80
81QRect QgsFontTextureAtlas::rect( const QString &grapheme ) const
82{
83 auto it = mGraphemeIndices.constFind( grapheme );
84 if ( it == mGraphemeIndices.constEnd() )
85 return QRect();
86
87 return mRects[it.value()].asQRect();
88}
89
90int QgsFontTextureAtlas::graphemeCount( const QString &string ) const
91{
92 auto it = mStringMetrics.constFind( string );
93 if ( it == mStringMetrics.constEnd() )
94 return 0;
95
96 return it->graphemeMetrics.count();
97}
98
99int QgsFontTextureAtlas::totalWidth( const QString &string ) const
100{
101 auto it = mStringMetrics.constFind( string );
102 if ( it == mStringMetrics.constEnd() )
103 return 0;
104
105 return it->totalWidth;
106}
107
108QPoint QgsFontTextureAtlas::pixelOffsetForGrapheme( const QString &string, int graphemeIndex ) const
109{
110 auto it = mStringMetrics.constFind( string );
111 if ( it == mStringMetrics.constEnd() )
112 return QPoint();
113
114 const GraphemeMetric &graphemeMetrics = it.value().graphemeMetrics[graphemeIndex];
115 auto charIt = mGraphemeIndices.constFind( graphemeMetrics.grapheme );
116 if ( charIt == mGraphemeIndices.constEnd() )
117 return QPoint();
118
119 return QPoint( graphemeMetrics.horizontalAdvance, -( mRects[charIt.value()].boundingRectSize.height() + mRects[charIt.value()].characterOffsetFromOrigin.y() ) );
120}
121
122QRect QgsFontTextureAtlas::textureRectForGrapheme( const QString &string, int graphemeIndex ) const
123{
124 auto it = mStringMetrics.constFind( string );
125 if ( it == mStringMetrics.constEnd() )
126 return QRect();
127
128 const GraphemeMetric &graphemeMetrics = it.value().graphemeMetrics.value( graphemeIndex );
129 auto charIt = mGraphemeIndices.constFind( graphemeMetrics.grapheme );
130 if ( charIt == mGraphemeIndices.constEnd() )
131 return QRect();
132
133 return mRects[charIt.value()].asQRect();
134}
135
137{
138 if ( mAtlasSize.isEmpty() )
139 return QImage();
140
141 QImage res( mAtlasSize, QImage::Format_ARGB32_Premultiplied );
142 res.fill( Qt::transparent );
143
144 QPainter painter( &res );
146 for ( const QgsCharTextureRect &rect : mRects )
147 {
148 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 );
149 }
150 painter.end();
151
152 return res;
153}
154
156{
157 if ( mAtlasSize.isEmpty() )
158 return QImage();
159
160 QImage res( mAtlasSize, QImage::Format_ARGB32_Premultiplied );
161 res.fill( Qt::transparent );
162
163 QPainter painter( &res );
164 painter.setPen( Qt::NoPen );
166 ramp.setTotalColorCount( static_cast< int >( mRects.size() ) );
167 double index = 0;
168 for ( const QgsCharTextureRect &rect : mRects )
169 {
170 const QColor color = ramp.color( index / ( static_cast< int >( mRects.size() ) - 1 ) );
171 index += 1;
172 painter.setBrush( QBrush( color ) );
173 painter.drawRect( rect.asQRect() );
174 }
175 painter.end();
176
177 return res;
178}
179
180
181//
182// QgsFontTextureAtlasGenerator
183//
184
186{
187 QgsRenderContext context;
188 context.setScaleFactor( 96.0 / 25.4 );
189 const QFontMetricsF fontMetrics = QgsTextRenderer::fontMetrics( context, format );
190
191 int texturePaddingPixels = 2;
192 if ( format.buffer().enabled() )
193 {
194 texturePaddingPixels += static_cast< int >( std::ceil( context.convertToPainterUnits( format.buffer().size(), format.buffer().sizeUnit() ) ) );
195 }
196
197 // collect unique graphemes from all strings
198 QSet<QString> uniqueGraphemes;
199 QMap< QString, QgsFontTextureAtlas::StringMetrics > stringMetrics;
200 for ( const QString &string : strings )
201 {
202 const QStringList graphemes = QgsPalLabeling::splitToGraphemes( string );
203
204 QgsFontTextureAtlas::StringMetrics thisStringMetrics;
205 thisStringMetrics.totalWidth = fontMetrics.boundingRect( string ).width();
206 thisStringMetrics.graphemeMetrics.reserve( graphemes.size() );
207 QString currentString;
208 for ( const QString &grapheme : graphemes )
209 {
210 uniqueGraphemes.insert( grapheme );
211 thisStringMetrics.graphemeMetrics << QgsFontTextureAtlas::GraphemeMetric( static_cast< int >( std::round( fontMetrics.horizontalAdvance( currentString ) ) ), grapheme );
212 currentString += grapheme;
213 }
214 stringMetrics.insert( string, thisStringMetrics );
215 }
216
217 if ( uniqueGraphemes.isEmpty() )
218 {
219 return QgsFontTextureAtlas();
220 }
221
222 // get bounding rectangles for all the unique characters we need to render
223 std::vector<QgsCharTextureRect> charRects;
224 charRects.reserve( uniqueGraphemes.size() );
225 for ( const QString &c : uniqueGraphemes )
226 {
227 const thread_local QRegularExpression sWhitespaceRx( QStringLiteral( "^\\s+$" ) );
228 if ( sWhitespaceRx.match( c ).hasMatch() )
229 continue;
230
231 const QRect boundingRect = c.size() == 1 ? fontMetrics.boundingRect( c.at( 0 ) ).toRect() : fontMetrics.boundingRect( c ).toRect();
232 charRects.emplace_back( QgsCharTextureRect( c, boundingRect.size(), boundingRect.topLeft(), texturePaddingPixels ) );
233 }
234
235 // pack character rects into an atlas
236 using spacesType = rectpack2D::empty_spaces<false, rectpack2D::default_empty_spaces>;
237
238 bool result = true;
239 auto reportSuccessful = []( rectpack2D::rect_xywh & ) {
240 return rectpack2D::callback_result::CONTINUE_PACKING;
241 };
242
243 auto reportUnsuccessful = [&result]( rectpack2D::rect_xywh & ) {
244 result = false;
245 return rectpack2D::callback_result::ABORT_PACKING;
246 };
247
248 const auto discardStep = -4;
249
250 auto byWidth = []( const rectpack2D::rect_xywh *a, const rectpack2D::rect_xywh *b ) {
251 return a->w > b->w;
252 };
253
254 const rectpack2D::rect_wh resultSize = rectpack2D::find_best_packing<spacesType>(
255 charRects,
256 rectpack2D::make_finder_input(
257 1024,
258 discardStep,
259 reportSuccessful,
260 reportUnsuccessful,
261 rectpack2D::flipping_option::DISABLED
262 ),
263 byWidth
264 );
265
266 if ( !result )
267 return QgsFontTextureAtlas();
268
270 res.mFormat = format;
271 res.mRects = std::move( charRects );
272 res.mAtlasSize = QSize( resultSize.w, resultSize.h );
273 res.mStringMetrics = std::move( stringMetrics );
274 res.mTexturePaddingPixels = texturePaddingPixels;
275
276 int index = 0;
277 for ( const QgsCharTextureRect &r : res.mRects )
278 {
279 res.mGraphemeIndices.insert( r.grapheme, index++ );
280 }
281
282 return res;
283}
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