1 /*
2  * filter_qtext.cpp -- text overlay filter
3  * Copyright (c) 2018-2021 Meltytech, LLC
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Lesser General Public
7  * License as published by the Free Software Foundation; either
8  * version 2.1 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Lesser General Public License for more details.
14  *
15  * You should have received a copy of the GNU Lesser General Public
16  * License along with this library; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18  */
19 
20 #include "common.h"
21 #include <framework/mlt.h>
22 #include <framework/mlt_log.h>
23 #include <QPainter>
24 #include <QPainterPath>
25 #include <QString>
26 #include <QFile>
27 #include <QTextDocument>
28 #include <QTextCodec>
29 #include <QMutexLocker>
30 
31 static QMutex g_mutex;
32 
get_text_path(QPainterPath * qpath,mlt_properties filter_properties,const char * text,double scale)33 static QRectF get_text_path( QPainterPath* qpath, mlt_properties filter_properties, const char* text, double scale )
34 {
35 	int outline = mlt_properties_get_int( filter_properties, "outline" ) * scale;
36 	char halign = mlt_properties_get( filter_properties, "halign" )[0];
37 	char style = mlt_properties_get( filter_properties, "style" )[0];
38 	int pad = mlt_properties_get_int( filter_properties, "pad" ) * scale;
39 	int offset = pad + ( outline / 2 );
40 	int width = 0;
41 	int height = 0;
42 
43 	qpath->setFillRule( Qt::WindingFill );
44 
45 	// Get the strings to display
46 	QString s = QString::fromUtf8(text);
47 	QStringList lines = s.split( "\n" );
48 
49 	// Configure the font
50 	QFont font;
51 	font.setPixelSize( mlt_properties_get_int( filter_properties, "size" ) * scale );
52 	font.setFamily( mlt_properties_get( filter_properties, "family" ) );
53 	font.setWeight( ( mlt_properties_get_int( filter_properties, "weight" ) / 10 ) -1 );
54 	switch( style )
55 	{
56 	case 'i':
57 	case 'I':
58 		font.setStyle( QFont::StyleItalic );
59 		break;
60 	}
61 	QFontMetrics fm( font );
62 
63 	// Determine the text rectangle size
64 	height = fm.lineSpacing() * lines.size();
65 	for( int i = 0; i < lines.size(); ++i )
66 	{
67 		const QString line = lines[i];
68 		int line_width = fm.width(line);
69 		int bearing = (line.size() > 0) ? fm.leftBearing(line.at(0)) : 0;
70 		if (bearing < 0)
71 			line_width -= bearing;
72 		bearing = (line.size() > 0) ? fm.rightBearing(line.at(line.size() - 1)) : 0;
73 		if (bearing < 0)
74 			line_width -= bearing;
75 		if (line_width > width)
76 			width = line_width;
77 	}
78 
79 	// Lay out the text in the path
80 	int x = 0;
81 	int y = fm.ascent() + offset;
82 	for( int i = 0; i < lines.size(); ++i )
83 	{
84 		QString line = lines.at(i);
85 		x = offset;
86 		int line_width = fm.width(line);
87 		int bearing = (line.size() > 0)? fm.leftBearing(line.at(0)) : 0;
88 
89 		if (bearing < 0) {
90 			line_width -= bearing;
91 			x -= bearing;
92 		}
93 		bearing = (line.size() > 0)? fm.rightBearing(line.at(line.size() - 1)) : 0;
94 		if (bearing < 0)
95 			line_width -= bearing;
96 
97 		switch( halign )
98 		{
99 			default:
100 			case 'l':
101 			case 'L':
102 				break;
103 			case 'c':
104 			case 'C':
105 				x += ( width - line_width ) / 2;
106 				break;
107 			case 'r':
108 			case 'R':
109 				x += width - line_width;
110 				break;
111 		}
112 		qpath->addText( x, y, font, line );
113 		y += fm.lineSpacing();
114 	}
115 
116 	// Account for outline and pad
117 	width += offset * 2;
118 	height += offset * 2;
119 	// Sanity check
120 	if( width == 0 ) width = 1;
121 	height += 2; // I found some fonts whose descenders get cut off.
122 
123 	return QRectF( 0, 0, width, height );
124 }
125 
get_qcolor(mlt_properties filter_properties,const char * name)126 static QColor get_qcolor( mlt_properties filter_properties, const char* name )
127 {
128 	mlt_color color = mlt_properties_get_color( filter_properties, name );
129 	return QColor( color.r, color.g, color.b, color.a );
130 }
131 
get_qpen(mlt_properties filter_properties)132 static QPen get_qpen( mlt_properties filter_properties )
133 {
134 	QColor color;
135 	int outline = mlt_properties_get_int( filter_properties, "outline" );
136 	QPen pen;
137 
138 	pen.setWidth( outline );
139 	if( outline )
140 	{
141 		color = get_qcolor( filter_properties, "olcolour" );
142 	}
143 	else
144 	{
145 		color = get_qcolor( filter_properties, "bgcolour" );
146 	}
147 	pen.setColor( color );
148 
149 	return pen;
150 }
151 
get_qbrush(mlt_properties filter_properties)152 static QBrush get_qbrush( mlt_properties filter_properties )
153 {
154 	QColor color = get_qcolor( filter_properties, "fgcolour" );
155 	return QBrush ( color );
156 }
157 
transform_painter(QPainter * painter,mlt_rect frame_rect,QRectF path_rect,mlt_properties filter_properties,mlt_profile profile)158 static void transform_painter( QPainter* painter, mlt_rect frame_rect, QRectF path_rect, mlt_properties filter_properties, mlt_profile profile )
159 {
160 	qreal sx = 1.0;
161 	qreal sy = mlt_profile_sar( profile );
162 	qreal path_width = path_rect.width() * sx;
163 	if( path_width > frame_rect.w )
164 	{
165 		sx *= frame_rect.w / path_width;
166 		sy *= frame_rect.w / path_width;
167 	}
168 	qreal path_height = path_rect.height() * sy;
169 	if( path_height > frame_rect.h )
170 	{
171 		sx *= frame_rect.h / path_height;
172 		sy *= frame_rect.h / path_height;
173 	}
174 
175 	qreal dx = frame_rect.x;
176 	qreal dy = frame_rect.y;
177 	char halign = mlt_properties_get( filter_properties, "halign" )[0];
178 	switch( halign )
179 	{
180 		default:
181 		case 'l':
182 		case 'L':
183 			break;
184 		case 'c':
185 		case 'C':
186 			dx += ( frame_rect.w - ( sx * path_rect.width() ) ) / 2;
187 			break;
188 		case 'r':
189 		case 'R':
190 			dx += frame_rect.w - ( sx * path_rect.width() );
191 			break;
192 	}
193 	char valign = mlt_properties_get( filter_properties, "valign" )[0];
194 	switch( valign )
195 	{
196 		default:
197 		case 't':
198 		case 'T':
199 			break;
200 		case 'm':
201 		case 'M':
202 			dy += ( frame_rect.h - ( sy * path_rect.height() ) ) / 2;
203 			break;
204 		case 'b':
205 		case 'B':
206 			dy += frame_rect.h - ( sy * path_rect.height() );
207 			break;
208 	}
209 
210 	QTransform transform;
211 	transform.translate( dx, dy );
212 	transform.scale( sx, sy );
213 	painter->setTransform( transform );
214 }
215 
paint_background(QPainter * painter,QRectF path_rect,mlt_properties filter_properties)216 static void paint_background( QPainter* painter, QRectF path_rect, mlt_properties filter_properties )
217 {
218 	QColor bg_color = get_qcolor( filter_properties, "bgcolour" );
219 	painter->fillRect( path_rect, bg_color );
220 }
221 
paint_text(QPainter * painter,QPainterPath * qpath,mlt_properties filter_properties)222 static void paint_text( QPainter* painter, QPainterPath* qpath, mlt_properties filter_properties )
223 {
224 	QPen pen = get_qpen( filter_properties );
225 	painter->setPen( pen );
226 	QBrush brush = get_qbrush( filter_properties );
227 	painter->setBrush( brush );
228 	painter->drawPath( *qpath );
229 }
230 
close_qtextdoc(void * p)231 static void close_qtextdoc(void* p)
232 {
233 	delete static_cast<QTextDocument*>(p);
234 }
235 
get_rich_text(mlt_properties properties,double width,double height)236 static QTextDocument* get_rich_text(mlt_properties properties, double width, double height)
237 {
238 	QTextDocument* doc = (QTextDocument*) mlt_properties_get_data(properties, "QTextDocument", NULL);
239 	auto html = QString::fromUtf8(mlt_properties_get(properties, "html"));
240 	auto prevHtml = QString::fromUtf8(mlt_properties_get(properties, "_html"));
241 	auto resource = QString::fromUtf8(mlt_properties_get(properties, "resource"));
242 	auto prevResource = QString::fromUtf8(mlt_properties_get(properties, "_resource"));
243 	auto prevWidth = mlt_properties_get_double(properties, "_width");
244 	auto prevHeight = mlt_properties_get_double(properties, "_height");
245 	bool changed = !doc || qAbs(width - prevWidth) > 1 || qAbs(height- prevHeight) > 1;
246 
247 	if (!resource.isEmpty() && (changed || resource != prevResource)) {
248 		QFile file(resource);
249 		if (file.open(QFile::ReadOnly)) {
250 			QByteArray data = file.readAll();
251 			QTextCodec *codec = QTextCodec::codecForHtml(data);
252 			doc = new QTextDocument;
253 			doc->setPageSize(QSizeF(width, height));
254 			doc->setHtml(codec->toUnicode(data));
255 			mlt_properties_set_data(properties, "QTextDocument", doc, 0, (mlt_destructor) close_qtextdoc, NULL);
256 			mlt_properties_set(properties, "_resource", resource.toUtf8().constData());
257 			mlt_properties_set_double(properties, "_width", width);
258 			mlt_properties_set_double(properties, "_height", height);
259 		}
260 	} else if (!html.isEmpty() && (changed || html != prevHtml)) {
261 //		fprintf(stderr, "%s\n", html.toUtf8().constData());
262 		doc = new QTextDocument;
263 		doc->setPageSize(QSizeF(width, height));
264 		doc->setHtml(html);
265 		mlt_properties_set_data(properties, "QTextDocument", doc, 0, (mlt_destructor) close_qtextdoc, NULL);
266 		mlt_properties_set(properties, "_html", html.toUtf8().constData());
267 		mlt_properties_set_double(properties, "_width", width);
268 		mlt_properties_set_double(properties, "_height", height);
269 	}
270 
271 	return doc;
272 }
273 
get_filter_properties(mlt_filter filter,mlt_frame frame)274 static mlt_properties get_filter_properties( mlt_filter filter, mlt_frame frame )
275 {
276 	mlt_properties properties = mlt_frame_get_unique_properties( frame, MLT_FILTER_SERVICE(filter) );
277 	if ( !properties )
278 		properties = MLT_FILTER_PROPERTIES(filter);
279 	return properties;
280 }
281 
filter_get_image(mlt_frame frame,uint8_t ** image,mlt_image_format * image_format,int * width,int * height,int writable)282 static int filter_get_image( mlt_frame frame, uint8_t **image, mlt_image_format *image_format, int *width, int *height, int writable )
283 {
284 	int error = 0;
285 	mlt_filter filter = (mlt_filter)mlt_frame_pop_service( frame );
286 	char* argument = (char*)mlt_frame_pop_service( frame );
287 	mlt_properties filter_properties = get_filter_properties( filter, frame );
288 	mlt_profile profile = mlt_service_profile(MLT_FILTER_SERVICE(filter));
289 	mlt_position position = mlt_filter_get_position( filter, frame );
290 	mlt_position length = mlt_filter_get_length2( filter, frame );
291 	bool isRichText = qstrlen(mlt_properties_get(filter_properties, "html")) > 0 ||
292 					  qstrlen(mlt_properties_get(filter_properties, "resource")) > 0;
293 	QString geom_str = QString::fromLatin1( mlt_properties_get( filter_properties, "geometry" ) );
294 	if( geom_str.isEmpty() )
295 	{
296 		free( argument );
297 		mlt_log_warning( MLT_FILTER_SERVICE(filter), "geometry property not set\n" );
298 		return mlt_frame_get_image( frame, image, image_format, width, height, writable );
299 	}
300 	mlt_rect rect = mlt_properties_anim_get_rect( filter_properties, "geometry", position, length );
301 
302 	// Get the current image
303 	*image_format = mlt_image_rgba;
304 	mlt_properties_set_int( MLT_FRAME_PROPERTIES(frame), "resize_alpha", 255 );
305 	mlt_service_lock(MLT_FILTER_SERVICE(filter));
306 	error = mlt_frame_get_image( frame, image, image_format, width, height, writable );
307 
308 	if( !error )
309 	{
310 		double scale = mlt_profile_scale_width(profile, *width);
311 		double scale_height = mlt_profile_scale_height(profile, *height);
312 		if ( geom_str.contains('%') )
313 		{
314 			rect.x *= *width;
315 			rect.w *= *width;
316 			rect.y *= *height;
317 			rect.h *= *height;
318 		}
319 		else
320 		{
321 			rect.x *= scale;
322 			rect.y *= scale_height;
323 			rect.w *= scale;
324 			rect.h *= scale_height;
325 		}
326 
327 		QImage qimg;
328 		convert_mlt_to_qimage_rgba( *image, &qimg, *width, *height );
329 
330 		QPainterPath text_path;
331 #ifdef Q_OS_WIN
332 		auto pixel_ratio = mlt_properties_get_double(filter_properties, "pixel_ratio");
333 #else
334 		auto pixel_ratio = 1.0;
335 #endif
336 		QRectF path_rect(0, 0, rect.w / scale * pixel_ratio, rect.h / scale_height * pixel_ratio);
337 		QPainter painter( &qimg );
338 		painter.setRenderHints( QPainter::Antialiasing | QPainter::TextAntialiasing | QPainter::HighQualityAntialiasing );
339 		if (isRichText) {
340 			auto overflowY = mlt_properties_exists(filter_properties, "overflow-y")?
341 				!!mlt_properties_get_int(filter_properties, "overflow-y") :
342 				(path_rect.height() >= profile->height * pixel_ratio);
343 			auto drawRect = overflowY? QRectF() : path_rect;
344 			QMutexLocker mutexLock(&g_mutex);
345 			auto doc = get_rich_text(filter_properties, path_rect.width(), std::numeric_limits<qreal>::max());
346 			if (doc) {
347 				transform_painter(&painter, rect, path_rect, filter_properties, profile);
348 				if (overflowY) {
349 					path_rect.setHeight(qMax(path_rect.height(), doc->size().height()));
350 				}
351 				paint_background(&painter, path_rect, filter_properties);
352 				doc->drawContents(&painter, drawRect);
353 			}
354 		} else {
355 			path_rect = get_text_path(&text_path, filter_properties, argument, scale);
356 			transform_painter(&painter, rect, path_rect, filter_properties, profile);
357 			paint_background(&painter, path_rect, filter_properties);
358 			paint_text(&painter, &text_path, filter_properties);
359 		}
360 		painter.end();
361 
362 		convert_qimage_to_mlt_rgba( &qimg, *image, *width, *height );
363 	}
364 	mlt_service_unlock(MLT_FILTER_SERVICE(filter));
365 	free( argument );
366 
367 	return error;
368 }
369 
370 /** Filter processing.
371 */
372 
filter_process(mlt_filter filter,mlt_frame frame)373 static mlt_frame filter_process( mlt_filter filter, mlt_frame frame )
374 {
375 	mlt_properties properties = get_filter_properties( filter, frame );
376 
377 	if (mlt_properties_get_int(properties, "_hide")) {
378 		return frame;
379 	}
380 
381 	char* argument = mlt_properties_get(properties, "argument");
382 	char* html = mlt_properties_get(properties, "html");
383 	char* resource = mlt_properties_get(properties, "resource");
384 
385 	// Save the text to be used by get_image() to support parallel processing
386 	// when this filter is encapsulated by other filters.
387 	if (qstrlen(resource)) {
388 		mlt_frame_push_service(frame, NULL);
389 	} else if (qstrlen(html)) {
390 		mlt_frame_push_service(frame, NULL);
391 	} else if (qstrlen(argument)) {
392 		mlt_frame_push_service(frame, strdup(argument));
393 	} else {
394 		return frame;
395 	}
396 
397 	// Push the filter on to the stack
398 	mlt_frame_push_service( frame, filter );
399 
400 	// Push the get_image on to the stack
401 	mlt_frame_push_get_image( frame, filter_get_image );
402 
403 	return frame;
404 }
405 
406 /** Constructor for the filter.
407 */
408 
409 extern "C" {
410 
filter_qtext_init(mlt_profile profile,mlt_service_type type,const char * id,char * arg)411 mlt_filter filter_qtext_init( mlt_profile profile, mlt_service_type type, const char *id, char *arg )
412 {
413 	mlt_filter filter = mlt_filter_new();
414 
415 	if( !filter ) return NULL;
416 
417 	if ( !createQApplicationIfNeeded( MLT_FILTER_SERVICE(filter) ) )  {
418 		mlt_filter_close( filter );
419 		return NULL;
420 	}
421 
422 	filter->process = filter_process;
423 
424 	mlt_properties filter_properties = MLT_FILTER_PROPERTIES( filter );
425 	// Assign default values
426 	mlt_properties_set_string( filter_properties, "argument", arg ? arg: "text" );
427 	mlt_properties_set_string( filter_properties, "geometry", "0%/0%:100%x100%:100%" );
428 	mlt_properties_set_string( filter_properties, "family", "Sans" );
429 	mlt_properties_set_string( filter_properties, "size", "48" );
430 	mlt_properties_set_string( filter_properties, "weight", "400" );
431 	mlt_properties_set_string( filter_properties, "style", "normal" );
432 	mlt_properties_set_string( filter_properties, "fgcolour", "0x000000ff" );
433 	mlt_properties_set_string( filter_properties, "bgcolour", "0x00000020" );
434 	mlt_properties_set_string( filter_properties, "olcolour", "0x00000000" );
435 	mlt_properties_set_string( filter_properties, "pad", "0" );
436 	mlt_properties_set_string( filter_properties, "halign", "left" );
437 	mlt_properties_set_string( filter_properties, "valign", "top" );
438 	mlt_properties_set_string( filter_properties, "outline", "0" );
439 	mlt_properties_set_double( filter_properties, "pixel_ratio", 1.0 );
440 	mlt_properties_set_int( filter_properties, "_filter_private", 1 );
441 
442 	return filter;
443 }
444 
445 }
446