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