Pass optimisation setting through to subtitle XML writers.
[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 "compose.hpp"
41 #include "dcp_assert.h"
42 #include "load_font_node.h"
43 #include "raw_convert.h"
44 #include "reel_asset.h"
45 #include "subtitle_asset.h"
46 #include "subtitle_asset_internal.h"
47 #include "subtitle_image.h"
48 #include "subtitle_string.h"
49 #include "util.h"
50 #include "xml.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 #include <algorithm>
58
59
60 using std::cerr;
61 using std::cout;
62 using std::dynamic_pointer_cast;
63 using std::make_shared;
64 using std::map;
65 using std::pair;
66 using std::shared_ptr;
67 using std::string;
68 using std::vector;
69 using boost::lexical_cast;
70 using boost::optional;
71 using namespace dcp;
72
73
74 SubtitleAsset::SubtitleAsset ()
75 {
76
77 }
78
79
80 SubtitleAsset::SubtitleAsset (boost::filesystem::path file)
81         : Asset (file)
82 {
83
84 }
85
86
87 string
88 string_attribute (xmlpp::Element const * node, string name)
89 {
90         auto a = node->get_attribute (name);
91         if (!a) {
92                 throw XMLError (String::compose ("missing attribute %1", name));
93         }
94         return string (a->get_value ());
95 }
96
97
98 optional<string>
99 optional_string_attribute (xmlpp::Element const * node, string name)
100 {
101         auto a = node->get_attribute (name);
102         if (!a) {
103                 return {};
104         }
105         return string (a->get_value ());
106 }
107
108
109 optional<bool>
110 optional_bool_attribute (xmlpp::Element const * node, string name)
111 {
112         auto s = optional_string_attribute (node, name);
113         if (!s) {
114                 return {};
115         }
116
117         return (s.get() == "1" || s.get() == "yes");
118 }
119
120
121 template <class T>
122 optional<T>
123 optional_number_attribute (xmlpp::Element const * node, string name)
124 {
125         auto s = optional_string_attribute (node, name);
126         if (!s) {
127                 return boost::optional<T> ();
128         }
129
130         std::string t = s.get ();
131         boost::erase_all (t, " ");
132         return raw_convert<T> (t);
133 }
134
135
136 SubtitleAsset::ParseState
137 SubtitleAsset::font_node_state (xmlpp::Element const * node, Standard standard) const
138 {
139         ParseState ps;
140
141         if (standard == Standard::INTEROP) {
142                 ps.font_id = optional_string_attribute (node, "Id");
143         } else {
144                 ps.font_id = optional_string_attribute (node, "ID");
145         }
146         ps.size = optional_number_attribute<int64_t> (node, "Size");
147         ps.aspect_adjust = optional_number_attribute<float> (node, "AspectAdjust");
148         ps.italic = optional_bool_attribute (node, "Italic");
149         ps.bold = optional_string_attribute(node, "Weight").get_value_or("normal") == "bold";
150         if (standard == Standard::INTEROP) {
151                 ps.underline = optional_bool_attribute (node, "Underlined");
152         } else {
153                 ps.underline = optional_bool_attribute (node, "Underline");
154         }
155         auto c = optional_string_attribute (node, "Color");
156         if (c) {
157                 ps.colour = Colour (c.get ());
158         }
159         auto const e = optional_string_attribute (node, "Effect");
160         if (e) {
161                 ps.effect = string_to_effect (e.get ());
162         }
163         c = optional_string_attribute (node, "EffectColor");
164         if (c) {
165                 ps.effect_colour = Colour (c.get ());
166         }
167
168         return ps;
169 }
170
171 void
172 SubtitleAsset::position_align (SubtitleAsset::ParseState& ps, xmlpp::Element const * node) const
173 {
174         auto hp = optional_number_attribute<float> (node, "HPosition");
175         if (!hp) {
176                 hp = optional_number_attribute<float> (node, "Hposition");
177         }
178         if (hp) {
179                 ps.h_position = hp.get () / 100;
180         }
181
182         auto ha = optional_string_attribute (node, "HAlign");
183         if (!ha) {
184                 ha = optional_string_attribute (node, "Halign");
185         }
186         if (ha) {
187                 ps.h_align = string_to_halign (ha.get ());
188         }
189
190         auto vp = optional_number_attribute<float> (node, "VPosition");
191         if (!vp) {
192                 vp = optional_number_attribute<float> (node, "Vposition");
193         }
194         if (vp) {
195                 ps.v_position = vp.get () / 100;
196         }
197
198         auto va = optional_string_attribute (node, "VAlign");
199         if (!va) {
200                 va = optional_string_attribute (node, "Valign");
201         }
202         if (va) {
203                 ps.v_align = string_to_valign (va.get ());
204         }
205
206         auto zp = optional_number_attribute<float>(node, "Zposition");
207         if (zp) {
208                 ps.z_position = zp.get() / 100;
209         }
210 }
211
212
213 SubtitleAsset::ParseState
214 SubtitleAsset::text_node_state (xmlpp::Element const * node) const
215 {
216         ParseState ps;
217
218         position_align (ps, node);
219
220         auto d = optional_string_attribute (node, "Direction");
221         if (d) {
222                 ps.direction = string_to_direction (d.get ());
223         }
224
225         ps.type = ParseState::Type::TEXT;
226
227         return ps;
228 }
229
230
231 SubtitleAsset::ParseState
232 SubtitleAsset::image_node_state (xmlpp::Element const * node) const
233 {
234         ParseState ps;
235
236         position_align (ps, node);
237
238         ps.type = ParseState::Type::IMAGE;
239
240         return ps;
241 }
242
243
244 SubtitleAsset::ParseState
245 SubtitleAsset::subtitle_node_state (xmlpp::Element const * node, optional<int> tcr) const
246 {
247         ParseState ps;
248         ps.in = Time (string_attribute(node, "TimeIn"), tcr);
249         ps.out = Time (string_attribute(node, "TimeOut"), tcr);
250         ps.fade_up_time = fade_time (node, "FadeUpTime", tcr);
251         ps.fade_down_time = fade_time (node, "FadeDownTime", tcr);
252         return ps;
253 }
254
255
256 Time
257 SubtitleAsset::fade_time (xmlpp::Element const * node, string name, optional<int> tcr) const
258 {
259         auto const u = optional_string_attribute(node, name).get_value_or ("");
260         Time t;
261
262         if (u.empty ()) {
263                 t = Time (0, 0, 0, 20, 250);
264         } else if (u.find (":") != string::npos) {
265                 t = Time (u, tcr);
266         } else {
267                 t = Time (0, 0, 0, lexical_cast<int> (u), tcr.get_value_or(250));
268         }
269
270         if (t > Time (0, 0, 8, 0, 250)) {
271                 t = Time (0, 0, 8, 0, 250);
272         }
273
274         return t;
275 }
276
277
278 void
279 SubtitleAsset::parse_subtitles (xmlpp::Element const * node, vector<ParseState>& state, optional<int> tcr, Standard standard)
280 {
281         if (node->get_name() == "Font") {
282                 state.push_back (font_node_state (node, standard));
283         } else if (node->get_name() == "Subtitle") {
284                 state.push_back (subtitle_node_state (node, tcr));
285         } else if (node->get_name() == "Text") {
286                 state.push_back (text_node_state (node));
287         } else if (node->get_name() == "SubtitleList") {
288                 state.push_back (ParseState ());
289         } else if (node->get_name() == "Image") {
290                 state.push_back (image_node_state (node));
291         } else {
292                 throw XMLError ("unexpected node " + node->get_name());
293         }
294
295         float space_before = 0;
296
297         /* Collect <Ruby>s first */
298         auto get_text_content = [](xmlpp::Element const* element) {
299                 string all_content;
300                 for (auto child: element->get_children()) {
301                         auto content = dynamic_cast<xmlpp::ContentNode const*>(child);
302                         if (content) {
303                                 all_content += content->get_content();
304                         }
305                 }
306                 return all_content;
307         };
308
309         vector<Ruby> rubies;
310         for (auto child: node->get_children()) {
311                 auto element = dynamic_cast<xmlpp::Element const*>(child);
312                 if (element && element->get_name() == "Ruby") {
313                         optional<string> base;
314                         optional<string> annotation;
315                         optional<float> size;
316                         optional<RubyPosition> position;
317                         optional<float> offset;
318                         optional<float> spacing;
319                         optional<float> aspect_adjust;
320                         for (auto ruby_child: element->get_children()) {
321                                 if (auto ruby_element = dynamic_cast<xmlpp::Element const*>(ruby_child)) {
322                                         if (ruby_element->get_name() == "Rb") {
323                                                 base = get_text_content(ruby_element);
324                                         } else if (ruby_element->get_name() == "Rt") {
325                                                 annotation = get_text_content(ruby_element);
326                                                 size = optional_number_attribute<float>(ruby_element, "Size");
327                                                 if (auto position_string = optional_string_attribute(ruby_element, "Position")) {
328                                                         if (*position_string == "before") {
329                                                                 position = RubyPosition::BEFORE;
330                                                         } else if (*position_string == "after") {
331                                                                 position = RubyPosition::AFTER;
332                                                         } else {
333                                                                 DCP_ASSERT(false);
334                                                         }
335                                                 }
336                                                 offset = optional_number_attribute<float>(ruby_element, "Offset");
337                                                 spacing = optional_number_attribute<float>(ruby_element, "Spacing");
338                                                 aspect_adjust = optional_number_attribute<float>(ruby_element, "AspectAdjust");
339                                         }
340                                 }
341                         }
342                         DCP_ASSERT(base);
343                         DCP_ASSERT(annotation);
344                         auto ruby = Ruby{*base, *annotation};
345                         if (size) {
346                                 ruby.size = *size;
347                         }
348                         if (position) {
349                                 ruby.position = *position;
350                         }
351                         if (offset) {
352                                 ruby.offset = *offset;
353                         }
354                         if (spacing) {
355                                 ruby.spacing = *spacing;
356                         }
357                         if (aspect_adjust) {
358                                 ruby.aspect_adjust = *aspect_adjust;
359                         }
360                         rubies.push_back(ruby);
361                 }
362         }
363
364         for (auto i: node->get_children()) {
365
366                 /* Handle actual content e.g. text */
367                 auto const v = dynamic_cast<xmlpp::ContentNode const *>(i);
368                 if (v) {
369                         maybe_add_subtitle (v->get_content(), state, space_before, standard, rubies);
370                         space_before = 0;
371                 }
372
373                 /* Handle other nodes */
374                 auto const e = dynamic_cast<xmlpp::Element const *>(i);
375                 if (e) {
376                         if (e->get_name() == "Space") {
377                                 if (node->get_name() != "Text") {
378                                         throw XMLError ("Space node found outside Text");
379                                 }
380                                 auto size = optional_string_attribute(e, "Size").get_value_or("0.5");
381                                 if (standard == dcp::Standard::INTEROP) {
382                                         boost::replace_all(size, "em", "");
383                                 }
384                                 space_before += raw_convert<float>(size);
385                         } else if (e->get_name() != "Ruby") {
386                                 parse_subtitles (e, state, tcr, standard);
387                         }
388                 }
389         }
390
391         state.pop_back ();
392 }
393
394
395 void
396 SubtitleAsset::maybe_add_subtitle(
397         string text,
398         vector<ParseState> const & parse_state,
399         float space_before,
400         Standard standard,
401         vector<Ruby> const& rubies
402         )
403 {
404         auto wanted = [](ParseState const& ps) {
405                 return ps.type && (ps.type.get() == ParseState::Type::TEXT || ps.type.get() == ParseState::Type::IMAGE);
406         };
407
408         if (find_if(parse_state.begin(), parse_state.end(), wanted) == parse_state.end()) {
409                 return;
410         }
411
412         ParseState ps;
413         for (auto const& i: parse_state) {
414                 if (i.font_id) {
415                         ps.font_id = i.font_id.get();
416                 }
417                 if (i.size) {
418                         ps.size = i.size.get();
419                 }
420                 if (i.aspect_adjust) {
421                         ps.aspect_adjust = i.aspect_adjust.get();
422                 }
423                 if (i.italic) {
424                         ps.italic = i.italic.get();
425                 }
426                 if (i.bold) {
427                         ps.bold = i.bold.get();
428                 }
429                 if (i.underline) {
430                         ps.underline = i.underline.get();
431                 }
432                 if (i.colour) {
433                         ps.colour = i.colour.get();
434                 }
435                 if (i.effect) {
436                         ps.effect = i.effect.get();
437                 }
438                 if (i.effect_colour) {
439                         ps.effect_colour = i.effect_colour.get();
440                 }
441                 if (i.h_position) {
442                         ps.h_position = i.h_position.get();
443                 }
444                 if (i.h_align) {
445                         ps.h_align = i.h_align.get();
446                 }
447                 if (i.v_position) {
448                         ps.v_position = i.v_position.get();
449                 }
450                 if (i.v_align) {
451                         ps.v_align = i.v_align.get();
452                 }
453                 if (i.z_position) {
454                         ps.z_position = i.z_position.get();
455                 }
456                 if (i.direction) {
457                         ps.direction = i.direction.get();
458                 }
459                 if (i.in) {
460                         ps.in = i.in.get();
461                 }
462                 if (i.out) {
463                         ps.out = i.out.get();
464                 }
465                 if (i.fade_up_time) {
466                         ps.fade_up_time = i.fade_up_time.get();
467                 }
468                 if (i.fade_down_time) {
469                         ps.fade_down_time = i.fade_down_time.get();
470                 }
471                 if (i.type) {
472                         ps.type = i.type.get();
473                 }
474         }
475
476         if (!ps.in || !ps.out) {
477                 /* We're not in a <Subtitle> node; just ignore this content */
478                 return;
479         }
480
481         DCP_ASSERT (ps.type);
482
483         switch (ps.type.get()) {
484         case ParseState::Type::TEXT:
485                 _subtitles.push_back (
486                         make_shared<SubtitleString>(
487                                 ps.font_id,
488                                 ps.italic.get_value_or (false),
489                                 ps.bold.get_value_or (false),
490                                 ps.underline.get_value_or (false),
491                                 ps.colour.get_value_or (dcp::Colour (255, 255, 255)),
492                                 ps.size.get_value_or (42),
493                                 ps.aspect_adjust.get_value_or (1.0),
494                                 ps.in.get(),
495                                 ps.out.get(),
496                                 ps.h_position.get_value_or(0),
497                                 ps.h_align.get_value_or(HAlign::CENTER),
498                                 ps.v_position.get_value_or(0),
499                                 ps.v_align.get_value_or(VAlign::CENTER),
500                                 ps.z_position.get_value_or(0),
501                                 ps.direction.get_value_or (Direction::LTR),
502                                 text,
503                                 ps.effect.get_value_or (Effect::NONE),
504                                 ps.effect_colour.get_value_or (dcp::Colour (0, 0, 0)),
505                                 ps.fade_up_time.get_value_or(Time()),
506                                 ps.fade_down_time.get_value_or(Time()),
507                                 space_before,
508                                 rubies
509                                 )
510                         );
511                 break;
512         case ParseState::Type::IMAGE:
513         {
514                 switch (standard) {
515                 case Standard::INTEROP:
516                         if (text.size() >= 4) {
517                                 /* Remove file extension */
518                                 text = text.substr(0, text.size() - 4);
519                         }
520                         break;
521                 case Standard::SMPTE:
522                         /* It looks like this urn:uuid: is required, but DoM wasn't expecting it (and not writing it)
523                          * until around 2.15.140 so I guess either:
524                          *   a) it is not (always) used in the field, or
525                          *   b) nobody noticed / complained.
526                          */
527                         if (text.substr(0, 9) == "urn:uuid:") {
528                                 text = text.substr(9);
529                         }
530                         break;
531                 }
532
533                 /* Add a subtitle with no image data and we'll fill that in later */
534                 _subtitles.push_back (
535                         make_shared<SubtitleImage>(
536                                 ArrayData(),
537                                 text,
538                                 ps.in.get(),
539                                 ps.out.get(),
540                                 ps.h_position.get_value_or(0),
541                                 ps.h_align.get_value_or(HAlign::CENTER),
542                                 ps.v_position.get_value_or(0),
543                                 ps.v_align.get_value_or(VAlign::CENTER),
544                                 ps.z_position.get_value_or(0),
545                                 ps.fade_up_time.get_value_or(Time()),
546                                 ps.fade_down_time.get_value_or(Time())
547                                 )
548                         );
549                 break;
550         }
551         }
552 }
553
554
555 vector<shared_ptr<const Subtitle>>
556 SubtitleAsset::subtitles () const
557 {
558         vector<shared_ptr<const Subtitle>> s;
559         for (auto i: _subtitles) {
560                 s.push_back (i);
561         }
562         return s;
563 }
564
565
566 vector<shared_ptr<const Subtitle>>
567 SubtitleAsset::subtitles_during (Time from, Time to, bool starting) const
568 {
569         vector<shared_ptr<const Subtitle>> s;
570         for (auto i: _subtitles) {
571                 if ((starting && from <= i->in() && i->in() < to) || (!starting && i->out() >= from && i->in() <= to)) {
572                         s.push_back (i);
573                 }
574         }
575
576         return s;
577 }
578
579
580 void
581 SubtitleAsset::add (shared_ptr<Subtitle> s)
582 {
583         _subtitles.push_back (s);
584 }
585
586
587 Time
588 SubtitleAsset::latest_subtitle_out () const
589 {
590         Time t;
591         for (auto i: _subtitles) {
592                 if (i->out() > t) {
593                         t = i->out ();
594                 }
595         }
596
597         return t;
598 }
599
600
601 bool
602 SubtitleAsset::equals(shared_ptr<const Asset> other_asset, EqualityOptions const& options, NoteHandler note) const
603 {
604         if (!Asset::equals (other_asset, options, note)) {
605                 return false;
606         }
607
608         auto other = dynamic_pointer_cast<const SubtitleAsset> (other_asset);
609         if (!other) {
610                 return false;
611         }
612
613         if (_subtitles.size() != other->_subtitles.size()) {
614                 note (NoteType::ERROR, String::compose("different number of subtitles: %1 vs %2", _subtitles.size(), other->_subtitles.size()));
615                 return false;
616         }
617
618         auto i = _subtitles.begin();
619         auto j = other->_subtitles.begin();
620
621         while (i != _subtitles.end()) {
622                 auto string_i = dynamic_pointer_cast<SubtitleString> (*i);
623                 auto string_j = dynamic_pointer_cast<SubtitleString> (*j);
624                 auto image_i = dynamic_pointer_cast<SubtitleImage> (*i);
625                 auto image_j = dynamic_pointer_cast<SubtitleImage> (*j);
626
627                 if ((string_i && !string_j) || (image_i && !image_j)) {
628                         note (NoteType::ERROR, "subtitles differ: string vs. image");
629                         return false;
630                 }
631
632                 if (string_i && !string_i->equals(string_j, options, note)) {
633                         return false;
634                 }
635
636                 if (image_i && !image_i->equals(image_j, options, note)) {
637                         return false;
638                 }
639
640                 ++i;
641                 ++j;
642         }
643
644         return true;
645 }
646
647
648 struct SubtitleSorter
649 {
650         bool operator() (shared_ptr<Subtitle> a, shared_ptr<Subtitle> b) {
651                 if (a->in() != b->in()) {
652                         return a->in() < b->in();
653                 }
654                 if (a->v_align() == VAlign::BOTTOM) {
655                         return a->v_position() > b->v_position();
656                 }
657                 return a->v_position() < b->v_position();
658         }
659 };
660
661
662 void
663 SubtitleAsset::pull_fonts (shared_ptr<order::Part> part)
664 {
665         if (part->children.empty ()) {
666                 return;
667         }
668
669         /* Pull up from children */
670         for (auto i: part->children) {
671                 pull_fonts (i);
672         }
673
674         if (part->parent) {
675                 /* Establish the common font features that each of part's children have;
676                    these features go into part's font.
677                 */
678                 part->font = part->children.front()->font;
679                 for (auto i: part->children) {
680                         part->font.take_intersection (i->font);
681                 }
682
683                 /* Remove common values from part's children's fonts */
684                 for (auto i: part->children) {
685                         i->font.take_difference (part->font);
686                 }
687         }
688
689         /* Merge adjacent children with the same font */
690         auto i = part->children.begin();
691         vector<shared_ptr<order::Part>> merged;
692
693         while (i != part->children.end()) {
694
695                 if ((*i)->font.empty ()) {
696                         merged.push_back (*i);
697                         ++i;
698                 } else {
699                         auto j = i;
700                         ++j;
701                         while (j != part->children.end() && (*i)->font == (*j)->font) {
702                                 ++j;
703                         }
704                         if (std::distance (i, j) == 1) {
705                                 merged.push_back (*i);
706                                 ++i;
707                         } else {
708                                 shared_ptr<order::Part> group (new order::Part (part, (*i)->font));
709                                 for (auto k = i; k != j; ++k) {
710                                         (*k)->font.clear ();
711                                         group->children.push_back (*k);
712                                 }
713                                 merged.push_back (group);
714                                 i = j;
715                         }
716                 }
717         }
718
719         part->children = merged;
720 }
721
722
723 /** @param standard Standard (INTEROP or SMPTE); this is used rather than putting things in the child
724  *  class because the differences between the two are fairly subtle.
725  */
726 void
727 SubtitleAsset::subtitles_as_xml(xmlpp::Element* xml_root, int time_code_rate, Standard standard, SubtitleOptimisation optimisation) const
728 {
729         auto sorted = _subtitles;
730         std::stable_sort(sorted.begin(), sorted.end(), SubtitleSorter());
731
732         /* Gather our subtitles into a hierarchy of Subtitle/Text/String objects, writing
733            font information into the bottom level (String) objects.
734         */
735
736         auto root = make_shared<order::Part>(shared_ptr<order::Part>());
737         shared_ptr<order::Subtitle> subtitle;
738         shared_ptr<order::Text> text;
739
740         Time last_in;
741         Time last_out;
742         Time last_fade_up_time;
743         Time last_fade_down_time;
744         HAlign last_h_align;
745         float last_h_position;
746         VAlign last_v_align;
747         float last_v_position;
748         float last_z_position;
749         Direction last_direction;
750
751         for (auto i: sorted) {
752                 if (!subtitle ||
753                     (last_in != i->in() ||
754                      last_out != i->out() ||
755                      last_fade_up_time != i->fade_up_time() ||
756                      last_fade_down_time != i->fade_down_time())
757                         ) {
758
759                         subtitle = make_shared<order::Subtitle>(root, i->in(), i->out(), i->fade_up_time(), i->fade_down_time());
760                         root->children.push_back (subtitle);
761
762                         last_in = i->in ();
763                         last_out = i->out ();
764                         last_fade_up_time = i->fade_up_time ();
765                         last_fade_down_time = i->fade_down_time ();
766                         text.reset ();
767                 }
768
769                 auto is = dynamic_pointer_cast<SubtitleString>(i);
770                 if (is) {
771                         if (!text ||
772                             last_h_align != is->h_align() ||
773                             fabs(last_h_position - is->h_position()) > ALIGN_EPSILON ||
774                             last_v_align != is->v_align() ||
775                             fabs(last_v_position - is->v_position()) > ALIGN_EPSILON ||
776                             fabs(last_z_position - is->z_position()) > ALIGN_EPSILON ||
777                             last_direction != is->direction()
778                                 ) {
779                                 text = make_shared<order::Text>(
780                                         subtitle,
781                                         is->h_align(),
782                                         is->h_position(),
783                                         is->v_align(),
784                                         is->v_position(),
785                                         is->z_position(),
786                                         is->direction(),
787                                         is->rubies()
788                                         );
789                                 subtitle->children.push_back (text);
790
791                                 last_h_align = is->h_align ();
792                                 last_h_position = is->h_position ();
793                                 last_v_align = is->v_align ();
794                                 last_v_position = is->v_position ();
795                                 last_z_position = is->z_position();
796                                 last_direction = is->direction ();
797                         }
798
799                         text->children.push_back (make_shared<order::String>(text, order::Font (is, standard), is->text(), is->space_before()));
800                 }
801
802                 auto ii = dynamic_pointer_cast<SubtitleImage>(i);
803                 if (ii) {
804                         text.reset ();
805                         subtitle->children.push_back (
806                                 make_shared<order::Image>(subtitle, ii->id(), ii->png_image(), ii->h_align(), ii->h_position(), ii->v_align(), ii->v_position(), ii->z_position())
807                                 );
808                 }
809         }
810
811         /* Pull font changes as high up the hierarchy as we can */
812
813         pull_fonts (root);
814
815         /* Write XML */
816
817         order::Context context(time_code_rate, standard, 1, optimisation);
818
819         root->write_xml (xml_root, context);
820 }
821
822
823 map<string, ArrayData>
824 SubtitleAsset::font_data () const
825 {
826         map<string, ArrayData> out;
827         for (auto const& i: _fonts) {
828                 out[i.load_id] = i.data;
829         }
830         return out;
831 }
832
833
834 map<string, boost::filesystem::path>
835 SubtitleAsset::font_filenames () const
836 {
837         map<string, boost::filesystem::path> out;
838         for (auto const& i: _fonts) {
839                 if (i.file) {
840                         out[i.load_id] = *i.file;
841                 }
842         }
843         return out;
844 }
845
846
847 /** Replace empty IDs in any <LoadFontId> and <Font> tags with
848  *  a dummy string.  Some systems give errors with empty font IDs
849  *  (see DCP-o-matic bug #1689).
850  */
851 void
852 SubtitleAsset::fix_empty_font_ids ()
853 {
854         bool have_empty = false;
855         vector<string> ids;
856         for (auto i: load_font_nodes()) {
857                 if (i->id == "") {
858                         have_empty = true;
859                 } else {
860                         ids.push_back (i->id);
861                 }
862         }
863
864         if (!have_empty) {
865                 return;
866         }
867
868         string const empty_id = unique_string (ids, "font");
869
870         for (auto i: load_font_nodes()) {
871                 if (i->id == "") {
872                         i->id = empty_id;
873                 }
874         }
875
876         for (auto i: _subtitles) {
877                 auto j = dynamic_pointer_cast<SubtitleString> (i);
878                 if (j && j->font() && j->font().get() == "") {
879                         j->set_font (empty_id);
880                 }
881         }
882 }
883
884
885 namespace {
886
887 struct State
888 {
889         int indent;
890         string xml;
891         int disable_formatting;
892 };
893
894 }
895
896
897 static
898 void
899 format_xml_node (xmlpp::Node const* node, State& state)
900 {
901         if (auto text_node = dynamic_cast<const xmlpp::TextNode*>(node)) {
902                 string content = text_node->get_content();
903                 boost::replace_all(content, "&", "&amp;");
904                 boost::replace_all(content, "<", "&lt;");
905                 boost::replace_all(content, ">", "&gt;");
906                 state.xml += content;
907         } else if (auto element = dynamic_cast<const xmlpp::Element*>(node)) {
908                 ++state.indent;
909
910                 auto children = element->get_children();
911                 auto const should_disable_formatting =
912                         std::any_of(
913                                 children.begin(), children.end(),
914                                 [](xmlpp::Node const* node) { return static_cast<bool>(dynamic_cast<const xmlpp::ContentNode*>(node)); }
915                                 ) || element->get_name() == "Text";
916
917                 if (!state.disable_formatting) {
918                         state.xml += "\n" + string(state.indent * 2, ' ');
919                 }
920
921                 state.xml += "<" + element->get_name();
922
923                 for (auto attribute: element->get_attributes()) {
924                         state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
925                 }
926
927                 if (children.empty()) {
928                         state.xml += "/>";
929                 } else {
930                         state.xml += ">";
931
932                         if (should_disable_formatting) {
933                                 ++state.disable_formatting;
934                         }
935
936                         for (auto child: children) {
937                                 format_xml_node(child, state);
938                         }
939
940                         if (!state.disable_formatting) {
941                                 state.xml += "\n" + string(state.indent * 2, ' ');
942                         }
943
944                         state.xml += String::compose("</%1>", element->get_name().raw());
945
946                         if (should_disable_formatting) {
947                                 --state.disable_formatting;
948                         }
949                 }
950
951                 --state.indent;
952         }
953 }
954
955
956 /** Format XML much as write_to_string_formatted() would do, except without adding any white space
957  *  to <Text> nodes.  This is an attempt to avoid changing what is actually displayed as subtitles
958  *  while also formatting the XML in such a way as to avoid DoM bug 2205.
959  *
960  *  xml_namespace is an optional namespace for the root node; it would be nicer to set this up with
961  *  set_namespace_declaration in the caller and then to extract it here but I couldn't find a way
962  *  to get all namespaces with the libxml++ API.
963  */
964 string
965 SubtitleAsset::format_xml(xmlpp::Document const& document, optional<pair<string, string>> xml_namespace)
966 {
967         auto root = document.get_root_node();
968
969         State state = {};
970         state.xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<" + root->get_name();
971
972         if (xml_namespace) {
973                 if (xml_namespace->first.empty()) {
974                         state.xml += String::compose(" xmlns=\"%1\"", xml_namespace->second);
975                 } else {
976                         state.xml += String::compose(" xmlns:%1=\"%2\"", xml_namespace->first, xml_namespace->second);
977                 }
978         }
979
980         for (auto attribute: root->get_attributes()) {
981                 state.xml += String::compose(" %1=\"%2\"", attribute->get_name().raw(), attribute->get_value().raw());
982         }
983
984         state.xml += ">";
985
986         for (auto child: document.get_root_node()->get_children()) {
987                 format_xml_node(child, state);
988         }
989
990         state.xml += String::compose("\n</%1>\n", root->get_name().raw());
991
992         return state.xml;
993 }
994
995
996 void
997 SubtitleAsset::ensure_font(string load_id, dcp::ArrayData data)
998 {
999         if (std::find_if(_fonts.begin(), _fonts.end(), [load_id](Font const& font) { return font.load_id == load_id; }) == _fonts.end()) {
1000                 add_font(load_id, data);
1001         }
1002 }
1003