Simple pass-through of <Ruby> tags in subtitles.
[libdcp.git] / test / test.cc
1 /*
2     Copyright (C) 2012-2020 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 #define BOOST_TEST_DYN_LINK
35 #define BOOST_TEST_MODULE libdcp_test
36 #include "compose.hpp"
37 #include "cpl.h"
38 #include "dcp.h"
39 #include "interop_subtitle_asset.h"
40 #include "file.h"
41 #include "j2k_transcode.h"
42 #include "mono_picture_asset.h"
43 #include "mono_picture_asset.h"
44 #include "openjpeg_image.h"
45 #include "picture_asset_writer.h"
46 #include "picture_asset_writer.h"
47 #include "reel.h"
48 #include "reel_asset.h"
49 #include "reel_interop_closed_caption_asset.h"
50 #include "reel_interop_subtitle_asset.h"
51 #include "reel_markers_asset.h"
52 #include "reel_mono_picture_asset.h"
53 #include "reel_mono_picture_asset.h"
54 #include "reel_smpte_closed_caption_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_sound_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "sound_asset.h"
59 #include "sound_asset_writer.h"
60 #include "test.h"
61 #include "util.h"
62 #include "warnings.h"
63 LIBDCP_DISABLE_WARNINGS
64 #include <asdcp/KM_util.h>
65 #include <asdcp/KM_prng.h>
66 LIBDCP_ENABLE_WARNINGS
67 #include <sndfile.h>
68 LIBDCP_DISABLE_WARNINGS
69 #include <libxml++/libxml++.h>
70 LIBDCP_ENABLE_WARNINGS
71 #include <boost/algorithm/string.hpp>
72 #include <boost/test/unit_test.hpp>
73 #include <cstdio>
74 #include <iostream>
75
76
77 using std::string;
78 using std::min;
79 using std::vector;
80 using std::shared_ptr;
81 using std::make_shared;
82 using boost::optional;
83
84
85 boost::filesystem::path private_test;
86 boost::filesystem::path xsd_test = "build/test/xsd with spaces";
87
88
89 struct TestConfig
90 {
91         TestConfig()
92         {
93                 dcp::init ();
94                 auto const argc = boost::unit_test::framework::master_test_suite().argc;
95                 auto const argv = boost::unit_test::framework::master_test_suite().argv;
96                 if (argc >= 3 && strcmp(argv[1], "--") == 0) {
97                         /* For some reason on Ubuntu 16.04 the -- we pass in ends up in argv[1] so we
98                          * need to get the private_test from argv[2]...
99                          */
100                         private_test = argv[2];
101                 } else if (argc >= 2) {
102                         /* ... but everywhere else it's removed, so we can just get the private_test
103                          * value from argv[1].
104                          */
105                         private_test = argv[1];
106                 }
107
108                 using namespace boost::filesystem;
109                 boost::system::error_code ec;
110                 remove_all (xsd_test, ec);
111                 boost::filesystem::create_directory (xsd_test);
112                 for (directory_iterator i = directory_iterator("xsd"); i != directory_iterator(); ++i) {
113                         copy_file (*i, xsd_test / i->path().filename());
114                 }
115         }
116 };
117
118
119 void
120 check_xml (xmlpp::Element* ref, xmlpp::Element* test, vector<string> ignore_tags, bool ignore_whitespace)
121 {
122         BOOST_CHECK_EQUAL (ref->get_name (), test->get_name ());
123         BOOST_CHECK_EQUAL (ref->get_namespace_prefix (), test->get_namespace_prefix ());
124
125         if (find(ignore_tags.begin(), ignore_tags.end(), ref->get_name()) != ignore_tags.end()) {
126                 return;
127         }
128
129         auto whitespace_content = [](xmlpp::Node* node) {
130                 auto content = dynamic_cast<xmlpp::ContentNode*>(node);
131                 return content && content->get_content().find_first_not_of(" \t\r\n") == string::npos;
132         };
133
134         auto ref_children = ref->get_children ();
135         auto test_children = test->get_children ();
136
137         auto k = ref_children.begin ();
138         auto l = test_children.begin ();
139         while (k != ref_children.end() && l != test_children.end()) {
140
141                 if (dynamic_cast<xmlpp::CommentNode*>(*k)) {
142                         ++k;
143                         continue;
144                 }
145
146                 if (dynamic_cast<xmlpp::CommentNode*>(*l)) {
147                         ++l;
148                         continue;
149                 }
150
151                 if (whitespace_content(*k) && ignore_whitespace) {
152                         ++k;
153                         continue;
154                 }
155
156                 if (whitespace_content(*l) && ignore_whitespace) {
157                         ++l;
158                         continue;
159                 }
160
161                 /* XXX: should be doing xmlpp::EntityReference, xmlpp::XIncludeEnd, xmlpp::XIncludeStart */
162
163                 auto ref_el = dynamic_cast<xmlpp::Element*> (*k);
164                 auto test_el = dynamic_cast<xmlpp::Element*> (*l);
165                 BOOST_CHECK ((ref_el && test_el) || (!ref_el && !test_el));
166                 if (ref_el && test_el) {
167                         check_xml (ref_el, test_el, ignore_tags, ignore_whitespace);
168                 }
169
170                 auto ref_cn = dynamic_cast<xmlpp::ContentNode*> (*k);
171                 auto test_cn = dynamic_cast<xmlpp::ContentNode*> (*l);
172                 BOOST_CHECK ((ref_cn && test_cn) || (!ref_cn && !test_cn));
173                 if (ref_cn && test_cn) {
174                         BOOST_CHECK_EQUAL (ref_cn->get_content(), test_cn->get_content());
175                 }
176
177                 ++k;
178                 ++l;
179         }
180
181         while (k != ref_children.end() && ignore_whitespace && whitespace_content(*k)) {
182                 ++k;
183         }
184
185         while (l != test_children.end() && ignore_whitespace && whitespace_content(*l)) {
186                 ++l;
187         }
188
189         BOOST_REQUIRE (k == ref_children.end());
190         BOOST_REQUIRE (l == test_children.end());
191
192         auto ref_attributes = ref->get_attributes ();
193         auto test_attributes = test->get_attributes ();
194         BOOST_CHECK_EQUAL (ref_attributes.size(), test_attributes.size ());
195
196         auto m = ref_attributes.begin();
197         auto n = test_attributes.begin();
198         while (m != ref_attributes.end ()) {
199                 BOOST_CHECK_EQUAL ((*m)->get_name(), (*n)->get_name());
200                 BOOST_CHECK_EQUAL ((*m)->get_value(), (*n)->get_value());
201
202                 ++m;
203                 ++n;
204         }
205 }
206
207 void
208 check_xml (string ref, string test, vector<string> ignore, bool ignore_whitespace)
209 {
210         auto ref_parser = new xmlpp::DomParser ();
211         ref_parser->parse_memory (ref);
212         auto ref_root = ref_parser->get_document()->get_root_node ();
213         auto test_parser = new xmlpp::DomParser ();
214         test_parser->parse_memory (test);
215         auto test_root = test_parser->get_document()->get_root_node ();
216
217         check_xml (ref_root, test_root, ignore, ignore_whitespace);
218 }
219
220 void
221 check_file (boost::filesystem::path ref, boost::filesystem::path check)
222 {
223         uintmax_t size = boost::filesystem::file_size (ref);
224         BOOST_CHECK_EQUAL (size, boost::filesystem::file_size(check));
225         dcp::File ref_file(ref, "rb");
226         BOOST_REQUIRE (ref_file);
227         dcp::File check_file(check, "rb");
228         BOOST_REQUIRE (check_file);
229
230         int const buffer_size = 65536;
231         std::vector<uint8_t> ref_buffer(buffer_size);
232         std::vector<uint8_t> check_buffer(buffer_size);
233
234         uintmax_t pos = 0;
235
236         while (pos < size) {
237                 uintmax_t this_time = min (uintmax_t(buffer_size), size - pos);
238                 size_t r = ref_file.read(ref_buffer.data(), 1, this_time);
239                 BOOST_CHECK_EQUAL (r, this_time);
240                 r = check_file.read(check_buffer.data(), 1, this_time);
241                 BOOST_CHECK_EQUAL (r, this_time);
242
243                 if (memcmp(ref_buffer.data(), check_buffer.data(), this_time) != 0) {
244                         for (int i = 0; i < buffer_size; ++i) {
245                                 if (ref_buffer[i] != check_buffer[i]) {
246                                         BOOST_CHECK_MESSAGE (
247                                                 false,
248                                                 dcp::String::compose("File %1 differs from reference %2 at offset %3", check, ref, pos + i)
249                                                 );
250                                         break;
251                                 }
252                         }
253                         break;
254                 }
255
256                 pos += this_time;
257         }
258 }
259
260
261 RNGFixer::RNGFixer ()
262 {
263         Kumu::cth_test = true;
264         Kumu::FortunaRNG().Reset();
265 }
266
267
268 RNGFixer::~RNGFixer ()
269 {
270         Kumu::cth_test = false;
271 }
272
273
274 shared_ptr<dcp::MonoPictureAsset>
275 simple_picture (boost::filesystem::path path, string suffix, int frames, optional<dcp::Key> key)
276 {
277         dcp::MXFMetadata mxf_meta;
278         mxf_meta.company_name = "OpenDCP";
279         mxf_meta.product_name = "OpenDCP";
280         mxf_meta.product_version = "0.0.25";
281
282         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
283         mp->set_metadata (mxf_meta);
284         if (key) {
285                 mp->set_key (*key);
286         }
287         auto picture_writer = mp->start_write(path / dcp::String::compose("video%1.mxf", suffix), dcp::PictureAsset::Behaviour::MAKE_NEW);
288
289         dcp::Size const size (1998, 1080);
290         auto image = make_shared<dcp::OpenJPEGImage>(size);
291         for (int i = 0; i < 3; ++i) {
292                 memset (image->data(i), 0, 2 * size.width * size.height);
293         }
294         auto j2c = dcp::compress_j2k (image, 100000000, 24, false, false);
295
296         for (int i = 0; i < frames; ++i) {
297                 picture_writer->write (j2c.data(), j2c.size());
298         }
299         picture_writer->finalize ();
300
301         return mp;
302 }
303
304
305 shared_ptr<dcp::SoundAsset>
306 simple_sound(boost::filesystem::path path, string suffix, dcp::MXFMetadata mxf_meta, string language, int frames, int sample_rate, optional<dcp::Key> key, int channels)
307 {
308         /* Set a valid language, then overwrite it, so that the language parameter can be badly formed */
309         auto ms = make_shared<dcp::SoundAsset>(dcp::Fraction(24, 1), sample_rate, channels, dcp::LanguageTag("en-US"), dcp::Standard::SMPTE);
310         if (key) {
311                 ms->set_key (*key);
312         }
313         ms->_language = language;
314         ms->set_metadata (mxf_meta);
315         auto sound_writer = ms->start_write(path / dcp::String::compose("audio%1.mxf", suffix), {}, dcp::SoundAsset::AtmosSync::DISABLED, dcp::SoundAsset::MCASubDescriptors::ENABLED);
316
317         int const samples_per_frame = sample_rate / 24;
318
319         float* silence[channels];
320         for (auto i = 0; i < channels; ++i) {
321                 silence[i] = new float[samples_per_frame];
322                 memset (silence[i], 0, samples_per_frame * sizeof(float));
323         }
324
325         for (auto i = 0; i < frames; ++i) {
326                 sound_writer->write(silence, channels, samples_per_frame);
327         }
328
329         sound_writer->finalize ();
330
331         for (auto i = 0; i < channels; ++i) {
332                 delete[] silence[i];
333         }
334
335         return ms;
336 }
337
338
339 shared_ptr<dcp::DCP>
340 make_simple (boost::filesystem::path path, int reels, int frames, dcp::Standard standard, optional<dcp::Key> key)
341 {
342         /* Some known metadata */
343         dcp::MXFMetadata mxf_meta;
344         mxf_meta.company_name = "OpenDCP";
345         mxf_meta.product_name = "OpenDCP";
346         mxf_meta.product_version = "0.0.25";
347
348         auto constexpr sample_rate = 48000;
349
350         boost::filesystem::remove_all (path);
351         boost::filesystem::create_directories (path);
352         auto d = make_shared<dcp::DCP>(path);
353         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, standard);
354         cpl->set_annotation_text ("A Test DCP");
355         cpl->set_issuer ("OpenDCP 0.0.25");
356         cpl->set_creator ("OpenDCP 0.0.25");
357         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
358         cpl->set_content_version (
359                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
360                 );
361         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
362         cpl->set_main_sound_sample_rate(sample_rate);
363         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
364         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
365         cpl->set_version_number(1);
366
367         for (int i = 0; i < reels; ++i) {
368                 string suffix = reels == 1 ? "" : dcp::String::compose("%1", i);
369
370                 auto mp = simple_picture (path, suffix, frames, key);
371                 auto ms = simple_sound (path, suffix, mxf_meta, "en-US", frames, sample_rate, key);
372
373                 auto reel = make_shared<dcp::Reel>(
374                         shared_ptr<dcp::ReelMonoPictureAsset>(new dcp::ReelMonoPictureAsset(mp, 0)),
375                         shared_ptr<dcp::ReelSoundAsset>(new dcp::ReelSoundAsset(ms, 0))
376                         );
377
378                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
379                 if (i == 0) {
380                         markers->set (dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
381                 }
382                 if (i == reels - 1) {
383                         markers->set (dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
384                 }
385                 reel->add (markers);
386
387                 cpl->add (reel);
388         }
389
390         d->set_annotation_text("A Test DCP");
391         d->add (cpl);
392         return d;
393 }
394
395
396 shared_ptr<dcp::Subtitle>
397 simple_subtitle ()
398 {
399         return std::make_shared<dcp::SubtitleString>(
400                 optional<string>(),
401                 false,
402                 false,
403                 false,
404                 dcp::Colour(255, 255, 255),
405                 42,
406                 1,
407                 dcp::Time(0, 0, 4, 0, 24),
408                 dcp::Time(0, 0, 8, 0, 24),
409                 0.5,
410                 dcp::HAlign::CENTER,
411                 0.8,
412                 dcp::VAlign::TOP,
413                 0,
414                 dcp::Direction::LTR,
415                 "Hello world",
416                 dcp::Effect::NONE,
417                 dcp::Colour(255, 255, 255),
418                 dcp::Time(),
419                 dcp::Time(),
420                 0,
421                 std::vector<dcp::Ruby>()
422                 );
423 }
424
425
426 shared_ptr<dcp::ReelMarkersAsset>
427 simple_markers (int frames)
428 {
429         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
430         markers->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
431         markers->set (dcp::Marker::LFOC, dcp::Time(frames - 1, 24, 24));
432         return markers;
433 }
434
435
436 shared_ptr<dcp::DCP>
437 make_simple_with_interop_subs (boost::filesystem::path path)
438 {
439         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
440
441         auto subs = make_shared<dcp::InteropSubtitleAsset>();
442         subs->add (simple_subtitle());
443
444         boost::filesystem::create_directory (path / "subs");
445         dcp::ArrayData data(4096);
446         memset(data.data(), 0, data.size());
447         subs->add_font ("afont", data);
448         subs->write (path / "subs" / "subs.xml");
449
450         auto reel_subs = make_shared<dcp::ReelInteropSubtitleAsset>(subs, dcp::Fraction(24, 1), 240, 0);
451         dcp->cpls().front()->reels().front()->add (reel_subs);
452
453         return dcp;
454 }
455
456
457 shared_ptr<dcp::DCP>
458 make_simple_with_smpte_subs (boost::filesystem::path path)
459 {
460         auto dcp = make_simple (path, 1, 192);
461
462         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
463         subs->set_language (dcp::LanguageTag("de-DE"));
464         subs->set_start_time (dcp::Time());
465         subs->add (simple_subtitle());
466         dcp::ArrayData fake_font(1024);
467         subs->add_font("font", fake_font);
468
469         subs->write (path / "subs.mxf");
470
471         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 192, 0);
472         dcp->cpls().front()->reels().front()->add (reel_subs);
473
474         return dcp;
475 }
476
477
478 shared_ptr<dcp::DCP>
479 make_simple_with_interop_ccaps (boost::filesystem::path path)
480 {
481         auto dcp = make_simple (path, 1, 24, dcp::Standard::INTEROP);
482
483         auto subs = make_shared<dcp::InteropSubtitleAsset>();
484         subs->add (simple_subtitle());
485         subs->write (path / "ccap.xml");
486
487         auto reel_caps = make_shared<dcp::ReelInteropClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 240, 0);
488         dcp->cpls()[0]->reels()[0]->add (reel_caps);
489
490         return dcp;
491 }
492
493
494 shared_ptr<dcp::DCP>
495 make_simple_with_smpte_ccaps (boost::filesystem::path path)
496 {
497         auto dcp = make_simple (path, 1, 192);
498
499         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
500         subs->set_language (dcp::LanguageTag("de-DE"));
501         subs->set_start_time (dcp::Time());
502         subs->add (simple_subtitle());
503         dcp::ArrayData fake_font(1024);
504         subs->add_font("font", fake_font);
505         subs->write (path / "ccap.mxf");
506
507         auto reel_caps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), 192, 0);
508         dcp->cpls()[0]->reels()[0]->add(reel_caps);
509
510         return dcp;
511 }
512
513
514 shared_ptr<dcp::OpenJPEGImage>
515 black_image (dcp::Size size)
516 {
517         auto image = make_shared<dcp::OpenJPEGImage>(size);
518         int const pixels = size.width * size.height;
519         for (int i = 0; i < 3; ++i) {
520                 memset (image->data(i), 0, pixels * sizeof(int));
521         }
522         return image;
523 }
524
525
526 shared_ptr<dcp::ReelAsset>
527 black_picture_asset (boost::filesystem::path dir, int frames)
528 {
529         auto image = black_image ();
530         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
531         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
532
533         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
534         asset->set_metadata (dcp::MXFMetadata("libdcp", "libdcp", "1.6.4devel"));
535         boost::filesystem::create_directories (dir);
536         auto writer = asset->start_write(dir / "pic.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
537         for (int i = 0; i < frames; ++i) {
538                 writer->write (frame.data(), frame.size());
539         }
540         writer->finalize ();
541
542         return make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
543 }
544
545
546 boost::filesystem::path
547 find_file (boost::filesystem::path dir, string filename_part)
548 {
549         boost::optional<boost::filesystem::path> found;
550         for (auto i: boost::filesystem::directory_iterator(dir)) {
551                 if (i.path().filename().string().find(filename_part) != string::npos) {
552                         BOOST_REQUIRE (!found);
553                         found = i;
554                 }
555         }
556         BOOST_REQUIRE (found);
557         return *found;
558 }
559
560
561 Editor::Editor(boost::filesystem::path path)
562         : _path(path)
563 {
564         _content = dcp::file_to_string (_path);
565 }
566
567
568 Editor::~Editor()
569 {
570         auto f = fopen(_path.string().c_str(), "w");
571         BOOST_REQUIRE (f);
572         fwrite (_content.c_str(), _content.length(), 1, f);
573         fclose (f);
574 }
575
576
577 void
578 Editor::replace(string a, string b)
579 {
580         ChangeChecker cc(this);
581         boost::algorithm::replace_all (_content, a, b);
582 }
583
584
585 void
586 Editor::delete_first_line_containing(string s)
587 {
588         ChangeChecker cc(this);
589         auto lines = as_lines();
590         _content = "";
591         bool done = false;
592         for (auto i: lines) {
593                 if (i.find(s) == string::npos || done) {
594                         _content += i + "\n";
595                 } else {
596                         done = true;
597                 }
598         }
599 }
600
601 void
602 Editor::delete_lines(string from, string to)
603 {
604         ChangeChecker cc(this);
605         auto lines = as_lines();
606         bool deleting = false;
607         _content = "";
608         for (auto i: lines) {
609                 if (i.find(from) != string::npos) {
610                         deleting = true;
611                 }
612                 if (!deleting) {
613                         _content += i + "\n";
614                 }
615                 if (deleting && i.find(to) != string::npos) {
616                         deleting = false;
617                 }
618         }
619 }
620
621
622 void
623 Editor::insert(string after, string line)
624 {
625         ChangeChecker cc(this);
626         auto lines = as_lines();
627         _content = "";
628         bool replaced = false;
629         for (auto i: lines) {
630                 _content += i + "\n";
631                 if (!replaced && i.find(after) != string::npos) {
632                         _content += line + "\n";
633                         replaced = true;
634                 }
635         }
636 }
637
638
639 void
640 Editor::delete_lines_after(string after, int lines_to_delete)
641 {
642         ChangeChecker cc(this);
643         auto lines = as_lines();
644         _content = "";
645         auto iter = std::find_if(lines.begin(), lines.end(), [after](string const& line) {
646                 return line.find(after) != string::npos;
647         });
648         int to_delete = 0;
649         for (auto i = lines.begin(); i != lines.end(); ++i) {
650                 if (i == iter) {
651                         to_delete = lines_to_delete;
652                         _content += *i + "\n";
653                 } else if (to_delete == 0) {
654                         _content += *i + "\n";
655                 } else {
656                         --to_delete;
657                 }
658         }
659 }
660
661
662 vector<string>
663 Editor::as_lines() const
664 {
665         vector<string> lines;
666         boost::algorithm::split(lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
667         return lines;
668 }
669
670
671 BOOST_GLOBAL_FIXTURE (TestConfig);