1 // Copyright 2005-2019 The Mumble Developers. All rights reserved.
2 // Use of this source code is governed by a BSD-style license
3 // that can be found in the LICENSE file at the root of the
4 // Mumble source tree or at <https://www.mumble.info/LICENSE>.
5 
6 #include "mumble_pch.hpp"
7 
8 #include "OverlayText.h"
9 
10 #include "Global.h"
11 
BasepointPixmap()12 BasepointPixmap::BasepointPixmap() :
13 		qpBasePoint(0, 0),
14 		iAscent(-1),
15 		iDescent(-1) { }
16 
BasepointPixmap(const QPixmap & pm)17 BasepointPixmap::BasepointPixmap(const QPixmap& pm) :
18 		QPixmap(pm),
19 		qpBasePoint(0, pm.height()),
20 		iAscent(-1),
21 		iDescent(-1) { }
22 
BasepointPixmap(int w,int h)23 BasepointPixmap::BasepointPixmap(int w, int h) :
24 		QPixmap(w, h),
25 		qpBasePoint(0, h),
26 		iAscent(-1),
27 		iDescent(-1) { }
28 
BasepointPixmap(int w,int h,const QPoint & bp)29 BasepointPixmap::BasepointPixmap(int w, int h, const QPoint& bp) :
30 		QPixmap(w, h),
31 		qpBasePoint(bp),
32 		iAscent(-1),
33 		iDescent(-1) { }
34 
OverlayTextLine(const QString & s,const QFont & f)35 OverlayTextLine::OverlayTextLine(const QString& s, const QFont& f)
36 	: fEdgeFactor(0.05f)
37 	, qsText(s)
38 	, qfFont(f)
39 	, fXCorrection(0.0)
40 	, fYCorrection(0.0)
41 	, iCurWidth(-1)
42 	, iCurHeight(-1)
43 	, fBaseliningThreshold(0.5f)
44 	, bElided(false) {
45 
46 	QFontMetrics fm(f);
47 	fAscent = static_cast<float>(fm.ascent());
48 	fDescent = static_cast<float>(fm.descent());
49 	fEdge = qMax(static_cast<float>(f.pointSizeF()) * fEdgeFactor, 1.0f);
50 }
51 
render(int w,int h,const QColor & col,const QPoint & bp) const52 BasepointPixmap OverlayTextLine::render(int w, int h, const QColor& col, const QPoint& bp) const {
53 	BasepointPixmap img(w, h, bp);
54 	img.fill(Qt::transparent);
55 
56 	QPainter imgp(&img);
57 	imgp.setRenderHint(QPainter::Antialiasing);
58 	imgp.setRenderHint(QPainter::TextAntialiasing);
59 	imgp.setBackground(QColor(0,0,0,0));
60 	imgp.setCompositionMode(QPainter::CompositionMode_SourceOver);
61 
62 	QColor qc(col);
63 	qc.setAlpha(255);
64 
65 	imgp.setBrush(qc);
66 	imgp.setPen(QPen(Qt::black, fEdge, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
67 	imgp.drawPath(qpp);
68 
69 	imgp.setPen(Qt::NoPen);
70 	imgp.drawPath(qpp);
71 
72 	img.iAscent = iroundf(fAscent + 0.5f);
73 	img.iDescent = iroundf(fDescent + 0.5f);
74 	return img;
75 }
76 
createPixmap(QColor col)77 BasepointPixmap OverlayTextLine::createPixmap(QColor col) {
78 	if (qsText.isEmpty()) {
79 		return BasepointPixmap();
80 	}
81 
82 	QRectF qr;
83 	if (qpp.isEmpty()) {
84 		qpp.addText(0.0f, fAscent, qfFont, qsText);
85 		qr = qpp.controlPointRect();
86 
87 		// fit into (0,0)-based coordinates
88 		fXCorrection = 0.0f;
89 		fYCorrection = 0.0f;
90 
91 		if (qr.left() < fEdge) {
92 			fXCorrection = fEdge - static_cast<float>(qr.left());
93 		}
94 
95 		if (qr.top() < fEdge) {
96 			fYCorrection = fEdge - static_cast<float>(qr.top());
97 		}
98 
99 		QMatrix correction;
100 		correction.translate(fXCorrection, fYCorrection);
101 		qpp = correction.map(qpp);
102 	}
103 
104 	qr = qpp.controlPointRect();
105 
106 	return render(
107 	           iroundf(qr.right() + 2.0f*fEdge + 0.5f),
108 	           iroundf(qr.bottom() + 2.0f*fEdge + 0.5f),
109 	           col,
110 	           QPoint(iroundf(fXCorrection + 0.5f), iroundf(fYCorrection + fAscent + 0.5f))
111 	       );
112 }
113 
createPixmap(unsigned int maxwidth,unsigned int height,QColor col)114 BasepointPixmap OverlayTextLine::createPixmap(unsigned int maxwidth, unsigned int height, QColor col) {
115 	float twice_edge = 2.0f * fEdge;
116 
117 	if (! height || ! maxwidth)
118 		return BasepointPixmap();
119 
120 	if (qpp.isEmpty() || iCurWidth > static_cast<int>(maxwidth) || iCurHeight != static_cast<int>(height) || (static_cast<int>(maxwidth) > iCurWidth && bElided)) {
121 		QFont f = qfFont;
122 		QFontMetrics fm(f);
123 
124 		// fit the font into a bounding box with padding
125 		float ps = static_cast<float>(f.pointSizeF());
126 		float f_ad = static_cast<float>(fm.ascent() + fm.descent()+1) / ps;
127 
128 		float pointsize = static_cast<float>(height) / (f_ad + 2.0f*fEdgeFactor);
129 
130 		if (fEdgeFactor * ps > 1.0f) {
131 			pointsize = static_cast<float>(height-2) / f_ad;
132 		}
133 
134 		if (pointsize <= 0.0f) {
135 			return BasepointPixmap();
136 		}
137 
138 		f.setPointSizeF(pointsize);
139 		setFont(f);
140 		fm = QFontMetrics(f);
141 		twice_edge = 2.0f * fEdge;
142 
143 		if (!qpp.isEmpty()) {
144 			qpp = QPainterPath();
145 		}
146 
147 		// calculate text metrics for eliding and scaling
148 		QRectF bb;
149 		qpp.addText(0.0f, 0.0f, f, qsText);
150 		bb = qpp.controlPointRect();
151 
152 		qreal effective_ascent = -bb.top();
153 		qreal effective_descent = bb.bottom();
154 		float scale = 1.0f;
155 		bool keep_baseline = true;
156 		if (effective_descent > fDescent || effective_ascent > fAscent) {
157 			qreal scale_ascent = effective_ascent > 0.0f ? fAscent / effective_ascent : 1.0f;
158 			qreal scale_descent = effective_descent > 0.0f ? fDescent / effective_descent : 1.0f;
159 			scale = static_cast<float>(qMin(scale_ascent, scale_descent));
160 
161 			if (scale < fBaseliningThreshold) {
162 				float text_height = static_cast<float>(bb.height()) + twice_edge;
163 				scale = static_cast<float>(height) / text_height;
164 				keep_baseline = false;
165 			}
166 
167 			qWarning() << QString(QLatin1String("Text \"%1\" did not fit (+%2/-%3): (+%4/-%5). Scaling to %6.")).arg(qsText).arg(fAscent).arg(fDescent).arg(effective_ascent).arg(effective_descent).arg(scale);
168 		}
169 
170 		// eliding by previously calculated width
171 		if ((bb.width()*scale) + twice_edge > maxwidth) {
172 			int eliding_width = iroundf((static_cast<float>(maxwidth) / scale) - twice_edge + 0.5f);
173 			QString str = fm.elidedText(qsText, Qt::ElideRight, eliding_width);
174 
175 			// use ellipsis as shortest possible string
176 			if (str.trimmed().isEmpty()) {
177 				str = QString(QChar(0x2026));
178 			}
179 
180 			qpp = QPainterPath();
181 			qpp.addText(0.0f, 0.0f, f, str);
182 			bb = qpp.controlPointRect();
183 			bElided = true;
184 		} else {
185 			bElided = false;
186 		}
187 
188 		// translation to "pixmap space":
189 		QMatrix correction;
190 		//  * adjust left edge
191 		correction.translate(-bb.x() + fEdge, 0.0f);
192 		//  * scale overly high text (still on baseline)
193 		correction.scale(scale, scale);
194 
195 		if (keep_baseline) {
196 			//  * translate down to baseline
197 			correction.translate(0.0f, (fAscent + fEdge) / scale);
198 		} else {
199 			//  * translate into bounding box
200 			correction.translate(0.0f, -bb.top() + fEdge);
201 		}
202 
203 		qpp = correction.map(qpp);
204 		iCurWidth = iroundf(bb.width() * scale + 0.5f);
205 		iCurHeight = height;
206 	}
207 
208 	QRectF qr = qpp.controlPointRect();
209 
210 	return render(
211 	           iroundf(qr.width() + twice_edge + 0.5f),
212 	           iroundf(fAscent + fDescent + twice_edge + 0.5f),
213 	           col,
214 	           QPoint(0, iroundf(fAscent + fEdge + 0.5f))
215 	       );
216 }
217 
setFont(const QFont & font)218 void OverlayTextLine::setFont(const QFont& font) {
219 	qfFont = font;
220 	qpp = QPainterPath();
221 	QFontMetrics fm(font);
222 	fAscent = static_cast<float>(fm.ascent()+1);
223 	fDescent = static_cast<float>(fm.descent()+1);
224 	fEdge = qMax(static_cast<float>(font.pointSizeF()) * fEdgeFactor, 1.0f);
225 }
226 
setEdge(float ew)227 void OverlayTextLine::setEdge(float ew) {
228 	fEdge = ew;
229 	qpp = QPainterPath();
230 }
231