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