Change subtitle SubtitleAsset API to return const Subtitle pointers; add subtitles_in...
[libdcp.git] / src / subtitle_asset.cc
1 /*
2     Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
6     libdcp is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     libdcp is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with libdcp.  If not, see <http://www.gnu.org/licenses/>.
18
19     In addition, as a special exception, the copyright holders give
20     permission to link the code of portions of this program with the
21     OpenSSL library under certain conditions as described in each
22     individual source file, and distribute linked combinations
23     including the two.
24
25     You must obey the GNU General Public License in all respects
26     for all of the code used other than OpenSSL.  If you modify
27     file(s) with this exception, you may extend this exception to your
28     version of the file(s), but you are not obligated to do so.  If you
29     do not wish to do so, delete this exception statement from your
30     version.  If you delete this exception statement from all source
31     files in the program, then also delete it here.
32 */
33
34 #include "raw_convert.h"
35 #include "compose.hpp"
36 #include "subtitle_asset.h"
37 #include "subtitle_asset_internal.h"
38 #include "util.h"
39 #include "xml.h"
40 #include "subtitle_string.h"
41 #include "subtitle_image.h"
42 #include "dcp_assert.h"
43 #include "load_font_node.h"
44 #include "reel_asset.h"
45 #include <asdcp/AS_DCP.h>
46 #include <asdcp/KM_util.h>
47 #include <libxml++/nodes/element.h>
48 #include <boost/algorithm/string.hpp>
49 #include <boost/lexical_cast.hpp>
50 #include <boost/shared_array.hpp>
51 #include <boost/foreach.hpp>
52
53 using std::dynamic_pointer_cast;
54 using std::string;
55 using std::cout;
56 using std::cerr;
57 using std::map;
58 using std::shared_ptr;
59 using std::vector;
60 using std::make_shared;
61 using boost::shared_array;
62 using boost::optional;
63 using boost::lexical_cast;
64 using namespace dcp;
65
66 SubtitleAsset::SubtitleAsset ()
67 {
68
69 }
70
71 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
72         : Asset (file)
73 {
74
75 }
76
77 string
78 string_attribute (xmlpp::Element const * node, string name)
79 {
80         xmlpp::Attribute* a = node->get_attribute (name);
81         if (!a) {
82                 throw XMLError (String::compose ("missing attribute %1", name));
83         }
84         return string (a->get_value ());
85 }
86
87 optional<string>
88 optional_string_attribute (xmlpp::Element const * node, string name)
89 {
90         xmlpp::Attribute* a = node->get_attribute (name);
91         if (!a) {
92                 return optional<string>();
93         }
94         return string (a->get_value ());
95 }
96
97 optional<bool>
98 optional_bool_attribute (xmlpp::Element const * node, string name)
99 {
100         optional<string> s = optional_string_attribute (node, name);
101         if (!s) {
102                 return optional<bool> ();
103         }
104
105         return (s.get() == "1" || s.get() == "yes");
106 }
107
108 template <class T>
109 optional<T>
110 optional_number_attribute (xmlpp::Element const * node, string name)
111 {
112         boost::optional<std::string> s = optional_string_attribute (node, name);
113         if (!s) {
114                 return boost::optional<T> ();
115         }
116
117         std::string t = s.get ();
118         boost::erase_all (t, " ");
119         return raw_convert<T> (t);
120 }
121
122 SubtitleAsset::ParseState
123 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
124 {
125         ParseState ps;
126
127         if (standard == INTEROP) {
128                 ps.font_id = optional_string_attribute (node, "Id");
129         } else {
130                 ps.font_id = optional_string_attribute (node, "ID");
131         }
132         ps.size = optional_number_attribute<int64_t> (node, "Size");
133         ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
134         ps.italic = optional_bool_attribute (node, "Italic");
135         ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
136         if (standard == INTEROP) {
137                 ps.underline = optional_bool_attribute (node, "Underlined");
138         } else {
139                 ps.underline = optional_bool_attribute (node, "Underline");
140         }
141         optional<string> c = optional_string_attribute (node, "Color");
142         if (c) {
143                 ps.colour = Colour (c.get ());
144         }
145         optional<string> const e = optional_string_attribute (node, "Effect");
146         if (e) {
147                 ps.effect = string_to_effect (e.get ());
148         }
149         c = optional_string_attribute (node, "EffectColor");
150         if (c) {
151                 ps.effect_colour = Colour (c.get ());
152         }
153
154         return ps;
155 }
156
157 void
158 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
159 {
160         optional<float> hp = optional_number_attribute<float> (node, "HPosition");
161         if (!hp) {
162                 hp = optional_number_attribute<float> (node, "Hposition");
163         }
164         if (hp) {
165                 ps.h_position = hp.get () / 100;
166         }
167
168         optional<string> ha = optional_string_attribute (node, "HAlign");
169         if (!ha) {
170                 ha = optional_string_attribute (node, "Halign");
171         }
172         if (ha) {
173                 ps.h_align = string_to_halign (ha.get ());
174         }
175
176         optional<float> vp = optional_number_attribute<float> (node, "VPosition");
177         if (!vp) {
178                 vp = optional_number_attribute<float> (node, "Vposition");
179         }
180         if (vp) {
181                 ps.v_position = vp.get () / 100;
182         }
183
184         optional<string> va = optional_string_attribute (node, "VAlign");
185         if (!va) {
186                 va = optional_string_attribute (node, "Valign");
187         }
188         if (va) {
189                 ps.v_align = string_to_valign (va.get ());
190         }
191
192 }
193
194 SubtitleAsset::ParseState
195 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
196 {
197         ParseState ps;
198
199         position_align (ps, node);
200
201         optional<string> d = optional_string_attribute (node, "Direction");
202         if (d) {
203                 ps.direction = string_to_direction (d.get ());
204         }
205
206         ps.type = ParseState::TEXT;
207
208         return ps;
209 }
210
211 SubtitleAsset::ParseState
212 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
213 {
214         ParseState ps;
215
216         position_align (ps, node);
217
218         ps.type = ParseState::IMAGE;
219
220         return ps;
221 }
222
223 SubtitleAsset::ParseState
224 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
225 {
226         ParseState ps;
227         ps.in = Time (string_attribute(node, "TimeIn"), tcr);
228         ps.out = Time (string_attribute(node, "TimeOut"), tcr);
229         ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
230         ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
231         return ps;
232 }
233
234 Time
235 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
236 {
237         string const u = optional_string_attribute(node, name).get_value_or ("");
238         Time t;
239
240         if (u.empty ()) {
241                 t = Time (0, 0, 0, 20, 250);
242         } else if (u.find (":") != string::npos) {
243                 t = Time (u, tcr);
244         } else {
245                 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
246         }
247
248         if (t > Time (0, 0, 8, 0, 250)) {
249                 t = Time (0, 0, 8, 0, 250);
250         }
251
252         return t;
253 }
254
255 void
256 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
257 {
258         if (node->get_name() == "Font") {
259                 state.push_back (font_node_state (node, standard));
260         } else if (node->get_name() == "Subtitle") {
261                 state.push_back (subtitle_node_state (node, tcr));
262         } else if (node->get_name() == "Text") {
263                 state.push_back (text_node_state (node));
264         } else if (node->get_name() == "SubtitleList") {
265                 state.push_back (ParseState ());
266         } else if (node->get_name() == "Image") {
267                 state.push_back (image_node_state (node));
268         } else {
269                 throw XMLError ("unexpected node " + node->get_name());
270         }
271
272         xmlpp::Node::NodeList c = node->get_children ();
273         for (xmlpp::Node::NodeList::const_iterator i = c.begin(); i != c.end(); ++i) {
274                 xmlpp::ContentNode const * v = dynamic_cast<xmlpp::ContentNode const *> (*i);
275                 if (v) {
276                         maybe_add_subtitle (v->get_content(), state, standard);
277                 }
278                 xmlpp::Element const * e = dynamic_cast<xmlpp::Element const *> (*i);
279                 if (e) {
280                         parse_subtitles (e, state, tcr, standard);
281                 }
282         }
283
284         state.pop_back ();
285 }
286
287 void
288 SubtitleAsset::maybe_add_subtitle (string text, vector<ParseState> const & parse_state, Standard standard)
289 {
290         if (empty_or_white_space (text)) {
291                 return;
292         }
293
294         ParseState ps;
295         BOOST_FOREACH (ParseState const & i, parse_state) {
296                 if (i.font_id) {
297                         ps.font_id = i.font_id.get();
298                 }
299                 if (i.size) {
300                         ps.size = i.size.get();
301                 }
302                 if (i.aspect_adjust) {
303                         ps.aspect_adjust = i.aspect_adjust.get();
304                 }
305                 if (i.italic) {
306                         ps.italic = i.italic.get();
307                 }
308                 if (i.bold) {
309                         ps.bold = i.bold.get();
310                 }
311                 if (i.underline) {
312                         ps.underline = i.underline.get();
313                 }
314                 if (i.colour) {
315                         ps.colour = i.colour.get();
316                 }
317                 if (i.effect) {
318                         ps.effect = i.effect.get();
319                 }
320                 if (i.effect_colour) {
321                         ps.effect_colour = i.effect_colour.get();
322                 }
323                 if (i.h_position) {
324                         ps.h_position = i.h_position.get();
325                 }
326                 if (i.h_align) {
327                         ps.h_align = i.h_align.get();
328                 }
329                 if (i.v_position) {
330                         ps.v_position = i.v_position.get();
331                 }
332                 if (i.v_align) {
333                         ps.v_align = i.v_align.get();
334                 }
335                 if (i.direction) {
336                         ps.direction = i.direction.get();
337                 }
338                 if (i.in) {
339                         ps.in = i.in.get();
340                 }
341                 if (i.out) {
342                         ps.out = i.out.get();
343                 }
344                 if (i.fade_up_time) {
345                         ps.fade_up_time = i.fade_up_time.get();
346                 }
347                 if (i.fade_down_time) {
348                         ps.fade_down_time = i.fade_down_time.get();
349                 }
350                 if (i.type) {
351                         ps.type = i.type.get();
352                 }
353         }
354
355         if (!ps.in || !ps.out) {
356                 /* We're not in a <Subtitle> node; just ignore this content */
357                 return;
358         }
359
360         DCP_ASSERT (ps.type);
361
362         switch (ps.type.get()) {
363         case ParseState::TEXT:
364                 _subtitles.push_back (
365                         shared_ptr<Subtitle> (
366                                 new SubtitleString (
367                                         ps.font_id,
368                                         ps.italic.get_value_or (false),
369                                         ps.bold.get_value_or (false),
370                                         ps.underline.get_value_or (false),
371                                         ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
372                                         ps.size.get_value_or (42),
373                                         ps.aspect_adjust.get_value_or (1.0),
374                                         ps.in.get(),
375                                         ps.out.get(),
376                                         ps.h_position.get_value_or(0),
377                                         ps.h_align.get_value_or(HALIGN_CENTER),
378                                         ps.v_position.get_value_or(0),
379                                         ps.v_align.get_value_or(VALIGN_CENTER),
380                                         ps.direction.get_value_or (DIRECTION_LTR),
381                                         text,
382                                         ps.effect.get_value_or (NONE),
383                                         ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
384                                         ps.fade_up_time.get_value_or(Time()),
385                                         ps.fade_down_time.get_value_or(Time())
386                                         )
387                                 )
388                         );
389                 break;
390         case ParseState::IMAGE:
391                 /* Add a subtitle with no image data and we'll fill that in later */
392                 _subtitles.push_back (
393                         shared_ptr<Subtitle> (
394                                 new SubtitleImage (
395                                         ArrayData (),
396                                         standard == INTEROP ? text.substr(0, text.size() - 4) : text,
397                                         ps.in.get(),
398                                         ps.out.get(),
399                                         ps.h_position.get_value_or(0),
400                                         ps.h_align.get_value_or(HALIGN_CENTER),
401                                         ps.v_position.get_value_or(0),
402                                         ps.v_align.get_value_or(VALIGN_CENTER),
403                                         ps.fade_up_time.get_value_or(Time()),
404                                         ps.fade_down_time.get_value_or(Time())
405                                         )
406                                 )
407                         );
408                 break;
409         }
410 }
411
412
413 vector<shared_ptr<const Subtitle>>
414 SubtitleAsset::subtitles () const
415 {
416         vector<shared_ptr<const Subtitle>> s;
417         for (auto i: _subtitles) {
418                 s.push_back (i);
419         }
420         return s;
421 }
422
423
424 vector<shared_ptr<const Subtitle>>
425 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
426 {
427         vector<shared_ptr<const Subtitle>> s;
428         for (auto i: _subtitles) {
429                 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
430                         s.push_back (i);
431                 }
432         }
433
434         return s;
435 }
436
437
438 /* XXX: this needs a test */
439 vector<shared_ptr<const Subtitle>>
440 SubtitleAsset::subtitles_in_reel (shared_ptr<const dcp::ReelAsset> asset) const
441 {
442         auto frame_rate = asset->edit_rate().as_float();
443         auto start = dcp::Time(asset->entry_point().get_value_or(0), frame_rate, time_code_rate());
444         auto during = subtitles_during (start, start + dcp::Time(asset->intrinsic_duration(), frame_rate, time_code_rate()), false);
445
446         vector<shared_ptr<const dcp::Subtitle>> corrected;
447         for (auto i: during) {
448                 auto c = make_shared<dcp::Subtitle>(*i);
449                 c->set_in (c->in() - start);
450                 c->set_out (c->out() - start);
451                 corrected.push_back (c);
452         }
453
454         return corrected;
455 }
456
457
458 void
459 SubtitleAsset::add (shared_ptr<Subtitle> s)
460 {
461         _subtitles.push_back (s);
462 }
463
464 Time
465 SubtitleAsset::latest_subtitle_out () const
466 {
467         Time t;
468         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
469                 if (i->out() > t) {
470                         t = i->out ();
471                 }
472         }
473
474         return t;
475 }
476
477 bool
478 SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const
479 {
480         if (!Asset::equals (other_asset, options, note)) {
481                 return false;
482         }
483
484         shared_ptr<const SubtitleAsset> other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
485         if (!other) {
486                 return false;
487         }
488
489         if (_subtitles.size() != other->_subtitles.size()) {
490                 note (DCP_ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
491                 return false;
492         }
493
494         auto i = _subtitles.begin();
495         auto j = other->_subtitles.begin();
496
497         while (i != _subtitles.end()) {
498                 shared_ptr<SubtitleString> string_i = dynamic_pointer_cast<SubtitleString> (*i);
499                 shared_ptr<SubtitleString> string_j = dynamic_pointer_cast<SubtitleString> (*j);
500                 shared_ptr<SubtitleImage> image_i = dynamic_pointer_cast<SubtitleImage> (*i);
501                 shared_ptr<SubtitleImage> image_j = dynamic_pointer_cast<SubtitleImage> (*j);
502
503                 if ((string_i && !string_j) || (image_i && !image_j)) {
504                         note (DCP_ERROR, "subtitles differ: string vs. image");
505                         return false;
506                 }
507
508                 if (string_i && *string_i != *string_j) {
509                         note (DCP_ERROR, String::compose("subtitles differ in text or metadata: %1 vs %2", string_i->text(), string_j->text()));
510                         return false;
511                 }
512
513                 if (image_i && !image_i->equals(image_j, options, note)) {
514                         return false;
515                 }
516
517                 ++i;
518                 ++j;
519         }
520
521         return true;
522 }
523
524 struct SubtitleSorter
525 {
526         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
527                 if (a->in() != b->in()) {
528                         return a->in() < b->in();
529                 }
530                 return a->v_position() < b->v_position();
531         }
532 };
533
534 void
535 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
536 {
537         if (part->children.empty ()) {
538                 return;
539         }
540
541         /* Pull up from children */
542         BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
543                 pull_fonts (i);
544         }
545
546         if (part->parent) {
547                 /* Establish the common font features that each of part's children have;
548                    these features go into part's font.
549                 */
550                 part->font = part->children.front()->font;
551                 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
552                         part->font.take_intersection (i->font);
553                 }
554
555                 /* Remove common values from part's children's fonts */
556                 BOOST_FOREACH (shared_ptr<order::Part> i, part->children) {
557                         i->font.take_difference (part->font);
558                 }
559         }
560
561         /* Merge adjacent children with the same font */
562         auto i = part->children.begin();
563         vector<shared_ptr<order::Part>> merged;
564
565         while (i != part->children.end()) {
566
567                 if ((*i)->font.empty ()) {
568                         merged.push_back (*i);
569                         ++i;
570                 } else {
571                         auto j = i;
572                         ++j;
573                         while (j != part->children.end() && (*i)->font == (*j)->font) {
574                                 ++j;
575                         }
576                         if (std::distance (i, j) == 1) {
577                                 merged.push_back (*i);
578                                 ++i;
579                         } else {
580                                 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
581                                 for (auto k = i; k != j; ++k) {
582                                         (*k)->font.clear ();
583                                         group->children.push_back (*k);
584                                 }
585                                 merged.push_back (group);
586                                 i = j;
587                         }
588                 }
589         }
590
591         part->children = merged;
592 }
593
594 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
595  *  class because the differences between the two are fairly subtle.
596  */
597 void
598 SubtitleAsset::subtitles_as_xml (xmlpp::Element* xml_root, int time_code_rate, Standard standard) const
599 {
600         vector<shared_ptr<Subtitle> > sorted = _subtitles;
601         std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
602
603         /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
604            font information into the bottom level (String) objects.
605         */
606
607         shared_ptr<order::Part> root (new order::Part (shared_ptr<order::Part> ()));
608         shared_ptr<order::Subtitle> subtitle;
609         shared_ptr<order::Text> text;
610
611         Time last_in;
612         Time last_out;
613         Time last_fade_up_time;
614         Time last_fade_down_time;
615         HAlign last_h_align;
616         float last_h_position;
617         VAlign last_v_align;
618         float last_v_position;
619         Direction last_direction;
620
621         BOOST_FOREACH (shared_ptr<Subtitle> i, sorted) {
622                 if (!subtitle ||
623                     (last_in != i->in() ||
624                      last_out != i->out() ||
625                      last_fade_up_time != i->fade_up_time() ||
626                      last_fade_down_time != i->fade_down_time())
627                         ) {
628
629                         subtitle.reset (new order::Subtitle (root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time()));
630                         root->children.push_back (subtitle);
631
632                         last_in = i->in ();
633                         last_out = i->out ();
634                         last_fade_up_time = i->fade_up_time ();
635                         last_fade_down_time = i->fade_down_time ();
636                         text.reset ();
637                 }
638
639                 shared_ptr<SubtitleString> is = dynamic_pointer_cast<SubtitleString>(i);
640                 if (is) {
641                         if (!text ||
642                             last_h_align != is->h_align() ||
643                             fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
644                             last_v_align != is->v_align() ||
645                             fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
646                             last_direction != is->direction()
647                                 ) {
648                                 text.reset (new order::Text (subtitle, is->h_align(), is->h_position(), is->v_align(), is->v_position(), is->direction()));
649                                 subtitle->children.push_back (text);
650
651                                 last_h_align = is->h_align ();
652                                 last_h_position = is->h_position ();
653                                 last_v_align = is->v_align ();
654                                 last_v_position = is->v_position ();
655                                 last_direction = is->direction ();
656                         }
657
658                         text->children.push_back (shared_ptr<order::String> (new order::String (text, order::Font (is, standard), is->text())));
659                 }
660
661                 shared_ptr<SubtitleImage> ii = dynamic_pointer_cast<SubtitleImage>(i);
662                 if (ii) {
663                         text.reset ();
664                         subtitle->children.push_back (
665                                 shared_ptr<order::Image> (new order::Image (subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position()))
666                                 );
667                 }
668         }
669
670         /* Pull font changes as high up the hierarchy as we can */
671
672         pull_fonts (root);
673
674         /* Write XML */
675
676         order::Context context;
677         context.time_code_rate = time_code_rate;
678         context.standard = standard;
679         context.spot_number = 1;
680
681         root->write_xml (xml_root, context);
682 }
683
684 map<string, ArrayData>
685 SubtitleAsset::font_data () const
686 {
687         map<string, ArrayData> out;
688         BOOST_FOREACH (Font const & i, _fonts) {
689                 out[i.load_id] = i.data;
690         }
691         return out;
692 }
693
694
695 map<string, boost::filesystem::path>
696 SubtitleAsset::font_filenames () const
697 {
698         map<string, boost::filesystem::path> out;
699         BOOST_FOREACH (Font const& i, _fonts) {
700                 if (i.file) {
701                         out[i.load_id] = *i.file;
702                 }
703         }
704         return out;
705 }
706
707
708 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
709  *  a dummy string.  Some systems give errors with empty font IDs
710  *  (see DCP-o-matic bug #1689).
711  */
712 void
713 SubtitleAsset::fix_empty_font_ids ()
714 {
715         bool have_empty = false;
716         vector<string> ids;
717         BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
718                 if (i->id == "") {
719                         have_empty = true;
720                 } else {
721                         ids.push_back (i->id);
722                 }
723         }
724
725         if (!have_empty) {
726                 return;
727         }
728
729         string const empty_id = unique_string (ids, "font");
730
731         BOOST_FOREACH (shared_ptr<LoadFontNode> i, load_font_nodes()) {
732                 if (i->id == "") {
733                         i->id = empty_id;
734                 }
735         }
736
737         BOOST_FOREACH (shared_ptr<Subtitle> i, _subtitles) {
738                 shared_ptr<SubtitleString> j = dynamic_pointer_cast<SubtitleString> (i);
739                 if (j && j->font() && j->font().get() == "") {
740                         j->set_font (empty_id);
741                 }
742         }
743 }