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