Don't add fonts to SMPTE closed caption MXFs (#2505).
[dcpomatic.git] / src / lib / reel_writer.cc
1 /*
2     Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic 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     DCP-o-matic 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 DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "audio_buffers.h"
23 #include "compose.hpp"
24 #include "config.h"
25 #include "constants.h"
26 #include "cross.h"
27 #include "dcpomatic_log.h"
28 #include "digester.h"
29 #include "film.h"
30 #include "image.h"
31 #include "image_png.h"
32 #include "job.h"
33 #include "log.h"
34 #include "reel_writer.h"
35 #include <dcp/atmos_asset.h>
36 #include <dcp/atmos_asset_writer.h>
37 #include <dcp/certificate_chain.h>
38 #include <dcp/cpl.h>
39 #include <dcp/dcp.h>
40 #include <dcp/interop_subtitle_asset.h>
41 #include <dcp/mono_picture_asset.h>
42 #include <dcp/raw_convert.h>
43 #include <dcp/reel.h>
44 #include <dcp/reel_atmos_asset.h>
45 #include <dcp/reel_interop_closed_caption_asset.h>
46 #include <dcp/reel_interop_subtitle_asset.h>
47 #include <dcp/reel_markers_asset.h>
48 #include <dcp/reel_mono_picture_asset.h>
49 #include <dcp/reel_smpte_closed_caption_asset.h>
50 #include <dcp/reel_smpte_subtitle_asset.h>
51 #include <dcp/reel_sound_asset.h>
52 #include <dcp/reel_stereo_picture_asset.h>
53 #include <dcp/smpte_subtitle_asset.h>
54 #include <dcp/sound_asset.h>
55 #include <dcp/sound_asset_writer.h>
56 #include <dcp/stereo_picture_asset.h>
57 #include <dcp/subtitle_image.h>
58
59 #include "i18n.h"
60
61
62 using std::dynamic_pointer_cast;
63 using std::exception;
64 using std::list;
65 using std::make_shared;
66 using std::map;
67 using std::set;
68 using std::shared_ptr;
69 using std::string;
70 using std::vector;
71 using std::weak_ptr;
72 using boost::optional;
73 #if BOOST_VERSION >= 106100
74 using namespace boost::placeholders;
75 #endif
76 using dcp::ArrayData;
77 using dcp::Data;
78 using dcp::raw_convert;
79 using namespace dcpomatic;
80
81
82 int const ReelWriter::_info_size = 48;
83
84
85 static dcp::MXFMetadata
86 mxf_metadata ()
87 {
88         dcp::MXFMetadata meta;
89         auto config = Config::instance();
90         if (!config->dcp_company_name().empty()) {
91                 meta.company_name = config->dcp_company_name ();
92         }
93         if (!config->dcp_product_name().empty()) {
94                 meta.product_name = config->dcp_product_name ();
95         }
96         if (!config->dcp_product_version().empty()) {
97                 meta.product_version = config->dcp_product_version ();
98         }
99         return meta;
100 }
101
102
103 /** @param job Related job, or 0.
104  *  @param text_only true to enable a special mode where the writer will expect only subtitles and closed captions to be written
105  *  (no picture nor sound) and not give errors in that case.  This is used by the hints system to check the potential sizes of
106  *  subtitle / closed caption files.
107  */
108 ReelWriter::ReelWriter (
109         weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count, bool text_only
110         )
111         : WeakConstFilm (weak_film)
112         , _period (period)
113         , _reel_index (reel_index)
114         , _reel_count (reel_count)
115         , _content_summary (film()->content_summary(period))
116         , _job (job)
117         , _text_only (text_only)
118         , _font_metrics(film()->frame_size().height)
119 {
120         /* Create or find our picture asset in a subdirectory, named
121            according to those film's parameters which affect the video
122            output.  We will hard-link it into the DCP later.
123         */
124
125         auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
126
127         boost::filesystem::path const asset =
128                 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
129
130         _first_nonexistent_frame = check_existing_picture_asset (asset);
131
132         if (_first_nonexistent_frame < period.duration().frames_round(film()->video_frame_rate())) {
133                 /* We do not have a complete picture asset.  If there is an
134                    existing asset, break any hard links to it as we are about
135                    to change its contents (if only by changing the IDs); see
136                    #1126.
137                 */
138                 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
139                         if (job) {
140                                 job->sub (_("Copying old video file"));
141                                 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
142                         } else {
143                                 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
144                         }
145                         boost::filesystem::remove (asset);
146                         boost::filesystem::rename (asset.string() + ".tmp", asset);
147                 }
148
149
150                 if (film()->three_d()) {
151                         _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
152                 } else {
153                         _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
154                 }
155
156                 _picture_asset->set_size (film()->frame_size());
157                 _picture_asset->set_metadata (mxf_metadata());
158
159                 if (film()->encrypted()) {
160                         _picture_asset->set_key (film()->key());
161                         _picture_asset->set_context_id (film()->context_id());
162                 }
163
164                 _picture_asset->set_file (asset);
165                 _picture_asset_writer = _picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::PictureAsset::Behaviour::OVERWRITE_EXISTING : dcp::PictureAsset::Behaviour::MAKE_NEW);
166         } else if (!text_only) {
167                 /* We already have a complete picture asset that we can just re-use */
168                 /* XXX: what about if the encryption key changes? */
169                 if (film()->three_d()) {
170                         _picture_asset = make_shared<dcp::StereoPictureAsset>(asset);
171                 } else {
172                         _picture_asset = make_shared<dcp::MonoPictureAsset>(asset);
173                 }
174         }
175
176         if (film()->audio_channels()) {
177                 auto lang = film()->audio_language();
178                 _sound_asset = make_shared<dcp::SoundAsset> (
179                         dcp::Fraction(film()->video_frame_rate(), 1),
180                         film()->audio_frame_rate(),
181                         /* Always make 16-channel sound assets for SMPTE; libdcp will pad unused channels for us */
182                         standard == dcp::Standard::SMPTE ? MAX_DCP_AUDIO_CHANNELS : film()->audio_channels(),
183                         lang ? *lang : dcp::LanguageTag("en-US"),
184                         standard
185                         );
186
187                 _sound_asset->set_metadata (mxf_metadata());
188
189                 if (film()->encrypted()) {
190                         _sound_asset->set_key (film()->key());
191                 }
192
193                 DCPOMATIC_ASSERT (film()->directory());
194
195                 auto mapped = film()->mapped_audio_channels();
196                 std::vector<dcp::Channel> extra_active_channels;
197                 auto add_if_mapped = [mapped, &extra_active_channels](dcp::Channel channel) {
198                         if (std::find(mapped.begin(), mapped.end(), static_cast<int>(channel)) != mapped.end()) {
199                                 extra_active_channels.push_back(channel);
200                         }
201                 };
202
203                 add_if_mapped(dcp::Channel::HI);
204                 add_if_mapped(dcp::Channel::VI);
205                 add_if_mapped(dcp::Channel::BSL);
206                 add_if_mapped(dcp::Channel::BSR);
207
208                 /* Write the sound asset into the film directory so that we leave the creation
209                    of the DCP directory until the last minute.
210                 */
211                 _sound_asset_writer = _sound_asset->start_write (
212                         film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
213                         extra_active_channels,
214                         film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
215                         film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
216                         );
217         }
218
219         _default_font = dcp::ArrayData(default_font_file());
220 }
221
222
223 /** @param frame reel-relative frame */
224 void
225 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
226 {
227         auto handle = film()->info_file_handle(_period, false);
228         handle->get().seek(frame_info_position(frame, eyes), SEEK_SET);
229         handle->get().checked_write(&info.offset, sizeof(info.offset));
230         handle->get().checked_write(&info.size, sizeof(info.size));
231         handle->get().checked_write(info.hash.c_str(), info.hash.size());
232 }
233
234
235 dcp::FrameInfo
236 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
237 {
238         dcp::FrameInfo frame_info;
239         info->get().seek(frame_info_position(frame, eyes), SEEK_SET);
240         info->get().checked_read(&frame_info.offset, sizeof(frame_info.offset));
241         info->get().checked_read(&frame_info.size, sizeof(frame_info.size));
242
243         char hash_buffer[33];
244         info->get().checked_read(hash_buffer, 32);
245         hash_buffer[32] = '\0';
246         frame_info.hash = hash_buffer;
247
248         return frame_info;
249 }
250
251
252 long
253 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
254 {
255         switch (eyes) {
256         case Eyes::BOTH:
257                 return frame * _info_size;
258         case Eyes::LEFT:
259                 return frame * _info_size * 2;
260         case Eyes::RIGHT:
261                 return frame * _info_size * 2 + _info_size;
262         default:
263                 DCPOMATIC_ASSERT (false);
264         }
265
266         DCPOMATIC_ASSERT (false);
267 }
268
269
270 Frame
271 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
272 {
273         auto job = _job.lock ();
274
275         if (job) {
276                 job->sub (_("Checking existing image data"));
277         }
278
279         /* Try to open the existing asset */
280         dcp::File asset_file(asset, "rb");
281         if (!asset_file) {
282                 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
283                 return 0;
284         } else {
285                 LOG_GENERAL ("Opened existing asset at %1", asset.string());
286         }
287
288         shared_ptr<InfoFileHandle> info_file;
289
290         try {
291                 info_file = film()->info_file_handle (_period, true);
292         } catch (OpenFileError &) {
293                 LOG_GENERAL_NC ("Could not open film info file");
294                 return 0;
295         }
296
297         /* Offset of the last dcp::FrameInfo in the info file */
298         int const n = (boost::filesystem::file_size(info_file->get().path()) / _info_size) - 1;
299         LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->get().path()), _info_size);
300
301         Frame first_nonexistent_frame;
302         if (film()->three_d()) {
303                 /* Start looking at the last left frame */
304                 first_nonexistent_frame = n / 2;
305         } else {
306                 first_nonexistent_frame = n;
307         }
308
309         while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
310                 --first_nonexistent_frame;
311         }
312
313         if (!film()->three_d() && first_nonexistent_frame > 0) {
314                 /* If we are doing 3D we might have found a good L frame with no R, so only
315                    do this if we're in 2D and we've just found a good B(oth) frame.
316                 */
317                 ++first_nonexistent_frame;
318         }
319
320         LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
321
322         return first_nonexistent_frame;
323 }
324
325
326 void
327 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
328 {
329         if (!_picture_asset_writer) {
330                 /* We're not writing any data */
331                 return;
332         }
333
334         auto fin = _picture_asset_writer->write (encoded->data(), encoded->size());
335         write_frame_info (frame, eyes, fin);
336         _last_written[eyes] = encoded;
337 }
338
339
340 void
341 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
342 {
343         if (!_atmos_asset) {
344                 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
345                 if (film()->encrypted()) {
346                         _atmos_asset->set_key(film()->key());
347                 }
348                 _atmos_asset_writer = _atmos_asset->start_write (
349                         film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
350                         );
351         }
352         _atmos_asset_writer->write (atmos);
353 }
354
355
356 void
357 ReelWriter::fake_write (int size)
358 {
359         if (!_picture_asset_writer) {
360                 /* We're not writing any data */
361                 return;
362         }
363
364         _picture_asset_writer->fake_write (size);
365 }
366
367
368 void
369 ReelWriter::repeat_write (Frame frame, Eyes eyes)
370 {
371         if (!_picture_asset_writer) {
372                 /* We're not writing any data */
373                 return;
374         }
375
376         auto fin = _picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size());
377         write_frame_info (frame, eyes, fin);
378 }
379
380
381 void
382 ReelWriter::finish (boost::filesystem::path output_dcp)
383 {
384         if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
385                 /* Nothing was written to the picture asset */
386                 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
387                 _picture_asset.reset ();
388         }
389
390         if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
391                 /* Nothing was written to the sound asset */
392                 _sound_asset.reset ();
393         }
394
395         /* Hard-link any video asset file into the DCP */
396         if (_picture_asset) {
397                 DCPOMATIC_ASSERT (_picture_asset->file());
398                 boost::filesystem::path video_from = _picture_asset->file().get();
399                 boost::filesystem::path video_to = output_dcp;
400                 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
401                 /* There may be an existing "to" file if we are recreating a DCP in the same place without
402                    changing any video.
403                 */
404                 boost::system::error_code ec;
405                 boost::filesystem::remove (video_to, ec);
406
407                 boost::filesystem::create_hard_link (video_from, video_to, ec);
408                 if (ec) {
409                         LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
410                         auto job = _job.lock ();
411                         if (job) {
412                                 job->sub (_("Copying video file into DCP"));
413                                 try {
414                                         copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
415                                 } catch (exception& e) {
416                                         LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
417                                         throw FileError (e.what(), video_from);
418                                 }
419                         } else {
420                                 boost::filesystem::copy_file (video_from, video_to, ec);
421                                 if (ec) {
422                                         LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
423                                         throw FileError (ec.message(), video_from);
424                                 }
425                         }
426                 }
427
428                 _picture_asset->set_file (video_to);
429         }
430
431         /* Move the audio asset into the DCP */
432         if (_sound_asset) {
433                 boost::filesystem::path audio_to = output_dcp;
434                 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
435                 audio_to /= aaf;
436
437                 boost::system::error_code ec;
438                 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
439                 if (ec) {
440                         throw FileError (
441                                 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
442                                 );
443                 }
444
445                 _sound_asset->set_file (audio_to);
446         }
447
448         if (_atmos_asset) {
449                 _atmos_asset_writer->finalize ();
450                 boost::filesystem::path atmos_to = output_dcp;
451                 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
452                 atmos_to /= aaf;
453
454                 boost::system::error_code ec;
455                 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
456                 if (ec) {
457                         throw FileError (
458                                 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
459                                 );
460                 }
461
462                 _atmos_asset->set_file (atmos_to);
463         }
464 }
465
466
467 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
468  *  A SubtitleAsset can be provided, or we will use one from @ref refs if not.
469  */
470 template <class Interop, class SMPTE, class Result>
471 shared_ptr<Result>
472 maybe_add_text (
473         shared_ptr<dcp::SubtitleAsset> asset,
474         int64_t picture_duration,
475         shared_ptr<dcp::Reel> reel,
476         int reel_index,
477         int reel_count,
478         optional<string> content_summary,
479         list<ReferencedReelAsset> const & refs,
480         FontIdMap const& fonts,
481         shared_ptr<dcpomatic::Font> chosen_interop_font,
482         dcp::ArrayData default_font,
483         shared_ptr<const Film> film,
484         DCPTimePeriod period,
485         boost::filesystem::path output_dcp,
486         bool text_only
487         )
488 {
489         Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
490
491         shared_ptr<Result> reel_asset;
492
493         if (asset) {
494                 if (!std::is_same<Result, dcp::ReelClosedCaptionAsset>::value) {
495                         if (film->interop()) {
496                                 if (chosen_interop_font) {
497                                         /* We only add one font, as Interop will ignore subsequent ones (and some validators will
498                                          * complain if they are even present)
499                                          */
500                                         asset->add_font(fonts.get(chosen_interop_font), chosen_interop_font->data().get_value_or(default_font));
501                                 }
502                         } else {
503                                 for (auto const& font: fonts.map()) {
504                                         asset->add_font(font.second, font.first->data().get_value_or(default_font));
505                                 }
506                         }
507                 }
508
509                 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
510                         auto directory = output_dcp / interop->id ();
511                         boost::filesystem::create_directories (directory);
512                         interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
513                         reel_asset = make_shared<Interop> (
514                                 interop,
515                                 dcp::Fraction(film->video_frame_rate(), 1),
516                                 picture_duration,
517                                 0
518                                 );
519                 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
520                         /* All our assets should be the same length; use the picture asset length here
521                            as a reference to set the subtitle one.  We'll use the duration rather than
522                            the intrinsic duration; we don't care if the picture asset has been trimmed, we're
523                            just interested in its presentation length.
524                         */
525                         smpte->set_intrinsic_duration(picture_duration);
526                         smpte->write (
527                                 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
528                                 );
529                         reel_asset = make_shared<SMPTE> (
530                                 smpte,
531                                 dcp::Fraction(film->video_frame_rate(), 1),
532                                 picture_duration,
533                                 0
534                                 );
535                 }
536
537         } else {
538                 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
539                 for (auto j: refs) {
540                         auto k = dynamic_pointer_cast<Result> (j.asset);
541                         if (k && j.period == period) {
542                                 reel_asset = k;
543                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
544                                 if (k->hash()) {
545                                         k->asset_ref()->set_hash (k->hash().get());
546                                 }
547                         }
548                 }
549         }
550
551         if (reel_asset) {
552                 if (!text_only && reel_asset->actual_duration() != period_duration) {
553                         throw ProgrammingError (
554                                 __FILE__, __LINE__,
555                                 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
556                                 );
557                 }
558                 reel->add (reel_asset);
559         }
560
561         return reel_asset;
562 }
563
564
565 shared_ptr<dcp::ReelPictureAsset>
566 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
567 {
568         shared_ptr<dcp::ReelPictureAsset> reel_asset;
569
570         if (_picture_asset) {
571                 /* We have made a picture asset of our own.  Put it into the reel */
572                 auto mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
573                 if (mono) {
574                         reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
575                 }
576
577                 auto stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
578                 if (stereo) {
579                         reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
580                 }
581         } else {
582                 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
583                 /* We don't have a picture asset of our own; hopefully we have one to reference */
584                 for (auto j: refs) {
585                         auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
586                         if (k) {
587                                 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
588                         }
589                         if (k && j.period == _period) {
590                                 reel_asset = k;
591                         }
592                 }
593         }
594
595         Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
596
597         DCPOMATIC_ASSERT (reel_asset);
598         if (reel_asset->duration() != period_duration) {
599                 throw ProgrammingError (
600                         __FILE__, __LINE__,
601                         String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
602                         );
603         }
604         reel->add (reel_asset);
605
606         /* If we have a hash for this asset in the CPL, assume that it is correct */
607         if (reel_asset->hash()) {
608                 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
609         }
610
611         return reel_asset;
612 }
613
614
615 void
616 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
617 {
618         shared_ptr<dcp::ReelSoundAsset> reel_asset;
619
620         if (_sound_asset) {
621                 /* We have made a sound asset of our own.  Put it into the reel */
622                 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
623         } else {
624                 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
625                 /* We don't have a sound asset of our own; hopefully we have one to reference */
626                 for (auto j: refs) {
627                         auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
628                         if (k) {
629                                 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
630                         }
631                         if (k && j.period == _period) {
632                                 reel_asset = k;
633                                 /* If we have a hash for this asset in the CPL, assume that it is correct */
634                                 if (k->hash()) {
635                                         k->asset_ref()->set_hash (k->hash().get());
636                                 }
637                         }
638                 }
639         }
640
641         auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
642
643         DCPOMATIC_ASSERT (reel_asset);
644         if (reel_asset->actual_duration() != period_duration) {
645                 LOG_ERROR (
646                         "Reel sound asset has length %1 but reel period is %2",
647                         reel_asset->actual_duration(),
648                         period_duration
649                         );
650                 if (reel_asset->actual_duration() != period_duration) {
651                         throw ProgrammingError (
652                                 __FILE__, __LINE__,
653                                 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
654                                 );
655                 }
656
657         }
658         reel->add (reel_asset);
659 }
660
661
662 void
663 ReelWriter::create_reel_text (
664         shared_ptr<dcp::Reel> reel,
665         list<ReferencedReelAsset> const & refs,
666         FontIdMap const& fonts,
667         shared_ptr<dcpomatic::Font> chosen_interop_font,
668         int64_t duration,
669         boost::filesystem::path output_dcp,
670         bool ensure_subtitles,
671         set<DCPTextTrack> ensure_closed_captions
672         ) const
673 {
674         auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
675                 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
676                 );
677
678         if (subtitle) {
679                 /* We have a subtitle asset that we either made or are referencing */
680                 if (auto main_language = film()->subtitle_languages().first) {
681                         subtitle->set_language (*main_language);
682                 }
683         } else if (ensure_subtitles) {
684                 /* We had no subtitle asset, but we've been asked to make sure there is one */
685                 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
686                         empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
687                         duration,
688                         reel,
689                         _reel_index,
690                         _reel_count,
691                         _content_summary,
692                         refs,
693                         fonts,
694                         chosen_interop_font,
695                         _default_font,
696                         film(),
697                         _period,
698                         output_dcp,
699                         _text_only
700                         );
701         }
702
703         for (auto const& i: _closed_caption_assets) {
704                 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
705                         i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, fonts, chosen_interop_font, _default_font, film(), _period, output_dcp, _text_only
706                         );
707                 DCPOMATIC_ASSERT (a);
708                 a->set_annotation_text (i.first.name);
709                 if (i.first.language) {
710                         a->set_language (i.first.language.get());
711                 }
712
713                 ensure_closed_captions.erase (i.first);
714         }
715
716         /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
717         for (auto i: ensure_closed_captions) {
718                 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
719                         empty_text_asset(TextType::CLOSED_CAPTION, i, true),
720                         duration,
721                         reel,
722                         _reel_index,
723                         _reel_count,
724                         _content_summary,
725                         refs,
726                         fonts,
727                         chosen_interop_font,
728                         _default_font,
729                         film(),
730                         _period,
731                         output_dcp,
732                         _text_only
733                         );
734                 DCPOMATIC_ASSERT (a);
735                 a->set_annotation_text (i.name);
736                 if (i.language) {
737                         a->set_language (i.language.get());
738                 }
739         }
740 }
741
742
743 void
744 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
745 {
746         auto markers = film()->markers();
747         film()->add_ffoc_lfoc(markers);
748         Film::Markers reel_markers;
749         for (auto const& i: markers) {
750                 if (_period.contains(i.second)) {
751                         reel_markers[i.first] = i.second;
752                 }
753         }
754
755         if (!reel_markers.empty ()) {
756                 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
757                 for (auto const& i: reel_markers) {
758                         DCPTime relative = i.second - _period.from;
759                         auto hmsf = relative.split (film()->video_frame_rate());
760                         ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
761                 }
762                 reel->add (ma);
763         }
764 }
765
766
767 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
768  *  @param ensure_closed_captions make sure the reel has these closed caption tracks.
769  */
770 shared_ptr<dcp::Reel>
771 ReelWriter::create_reel (
772         list<ReferencedReelAsset> const & refs,
773         FontIdMap const & fonts,
774         shared_ptr<dcpomatic::Font> chosen_interop_font,
775         boost::filesystem::path output_dcp,
776         bool ensure_subtitles,
777         set<DCPTextTrack> ensure_closed_captions
778         )
779 {
780         LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
781
782         auto reel = make_shared<dcp::Reel>();
783
784         /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
785          * how long the subtitle / CCAP assets should be.  However, since we're only writing them to see
786          * how big they are, we don't care about that.
787          */
788         int64_t duration = 0;
789         if (!_text_only) {
790                 auto reel_picture_asset = create_reel_picture (reel, refs);
791                 duration = reel_picture_asset->actual_duration ();
792                 create_reel_sound (reel, refs);
793                 create_reel_markers (reel);
794         }
795
796         create_reel_text (reel, refs, fonts, chosen_interop_font, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
797
798         if (_atmos_asset) {
799                 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
800         }
801
802         return reel;
803 }
804
805 void
806 ReelWriter::calculate_digests (std::function<void (float)> set_progress)
807 try
808 {
809         if (_picture_asset) {
810                 _picture_asset->hash (set_progress);
811         }
812
813         if (_sound_asset) {
814                 _sound_asset->hash (set_progress);
815         }
816
817         if (_atmos_asset) {
818                 _atmos_asset->hash (set_progress);
819         }
820 } catch (boost::thread_interrupted) {
821         /* set_progress contains an interruption_point, so any of these methods
822          * may throw thread_interrupted, at which point we just give up.
823          */
824 }
825
826
827 Frame
828 ReelWriter::start () const
829 {
830         return _period.from.frames_floor (film()->video_frame_rate());
831 }
832
833
834 void
835 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
836 {
837         if (!_sound_asset_writer) {
838                 return;
839         }
840
841         DCPOMATIC_ASSERT (audio);
842         _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
843 }
844
845
846 shared_ptr<dcp::SubtitleAsset>
847 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
848 {
849         shared_ptr<dcp::SubtitleAsset> asset;
850
851         auto lang = film()->subtitle_languages();
852         if (film()->interop()) {
853                 auto s = make_shared<dcp::InteropSubtitleAsset>();
854                 s->set_movie_title (film()->name());
855                 if (type == TextType::OPEN_SUBTITLE) {
856                         s->set_language (lang.first ? lang.first->to_string() : "Unknown");
857                 } else if (track->language) {
858                         s->set_language (track->language->to_string());
859                 }
860                 s->set_reel_number (raw_convert<string> (_reel_index + 1));
861                 asset = s;
862         } else {
863                 auto s = make_shared<dcp::SMPTESubtitleAsset>();
864                 s->set_content_title_text (film()->name());
865                 s->set_metadata (mxf_metadata());
866                 if (type == TextType::OPEN_SUBTITLE && lang.first) {
867                         s->set_language (*lang.first);
868                 } else if (track && track->language) {
869                         s->set_language (dcp::LanguageTag(track->language->to_string()));
870                 }
871                 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
872                 s->set_reel_number (_reel_index + 1);
873                 s->set_time_code_rate (film()->video_frame_rate());
874                 s->set_start_time (dcp::Time ());
875                 if (film()->encrypted()) {
876                         s->set_key (film()->key());
877                 }
878                 asset = s;
879         }
880
881         if (with_dummy) {
882                 asset->add(
883                         std::make_shared<dcp::SubtitleString>(
884                                 optional<std::string>(),
885                                 false,
886                                 false,
887                                 false,
888                                 dcp::Colour(),
889                                 42,
890                                 1.0,
891                                 dcp::Time(0, 0, 0, 0, 24),
892                                 dcp::Time(0, 0, 1, 0, 24),
893                                 0.5,
894                                 dcp::HAlign::CENTER,
895                                 0.5,
896                                 dcp::VAlign::CENTER,
897                                 0,
898                                 dcp::Direction::LTR,
899                                 " ",
900                                 dcp::Effect::NONE,
901                                 dcp::Colour(),
902                                 dcp::Time(),
903                                 dcp::Time(),
904                                 0
905                                 )
906                        );
907         }
908
909         return asset;
910 }
911
912
913 float
914 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
915 {
916         if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
917                 /* The from and to standards use the same alignment reference */
918                 return subtitle.v_position();
919         }
920
921         auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
922         auto const height = _font_metrics.height(subtitle);
923
924         float correction = 0;
925         switch (subtitle.v_align()) {
926         case dcp::VAlign::TOP:
927                 correction = height - baseline_to_bottom;
928                 break;
929         case dcp::VAlign::CENTER:
930                 correction = (height / 2) - baseline_to_bottom;
931                 break;
932         case dcp::VAlign::BOTTOM:
933                 correction = baseline_to_bottom;
934                 break;
935         }
936
937         return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
938 }
939
940
941 void
942 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts)
943 {
944         shared_ptr<dcp::SubtitleAsset> asset;
945
946         switch (type) {
947         case TextType::OPEN_SUBTITLE:
948                 asset = _subtitle_asset;
949                 break;
950         case TextType::CLOSED_CAPTION:
951                 DCPOMATIC_ASSERT (track);
952                 asset = _closed_caption_assets[*track];
953                 break;
954         default:
955                 DCPOMATIC_ASSERT (false);
956         }
957
958         if (!asset) {
959                 asset = empty_text_asset (type, track, false);
960         }
961
962         switch (type) {
963         case TextType::OPEN_SUBTITLE:
964                 _subtitle_asset = asset;
965                 break;
966         case TextType::CLOSED_CAPTION:
967                 DCPOMATIC_ASSERT (track);
968                 _closed_caption_assets[*track] = asset;
969                 break;
970         default:
971                 DCPOMATIC_ASSERT (false);
972         }
973
974         /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
975         auto const tcr = 1000;
976
977         for (auto i: subs.string) {
978                 i.set_in  (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
979                 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
980                 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
981                 auto sub = make_shared<dcp::SubtitleString>(i);
982                 if (type == TextType::OPEN_SUBTITLE) {
983                         sub->set_font(fonts.get(i.font));
984                 }
985                 asset->add(sub);
986         }
987
988         for (auto i: subs.bitmap) {
989                 asset->add (
990                         make_shared<dcp::SubtitleImage>(
991                                 image_as_png(i.image),
992                                 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
993                                 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
994                                 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
995                                 dcp::Time(), dcp::Time()
996                                 )
997                         );
998         }
999 }
1000
1001
1002 bool
1003 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
1004 {
1005         LOG_GENERAL ("Checking existing picture frame %1", frame);
1006
1007         /* Read the data from the info file; for 3D we just check the left
1008            frames until we find a good one.
1009         */
1010         auto const info = read_frame_info (info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
1011
1012         bool ok = true;
1013
1014         /* Read the data from the asset and hash it */
1015         asset_file.seek(info.offset, SEEK_SET);
1016         ArrayData data (info.size);
1017         size_t const read = asset_file.read(data.data(), 1, data.size());
1018         LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
1019         if (read != static_cast<size_t> (data.size ())) {
1020                 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
1021                 ok = false;
1022         } else {
1023                 Digester digester;
1024                 digester.add (data.data(), data.size());
1025                 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
1026                 if (digester.get() != info.hash) {
1027                         LOG_GENERAL ("Existing frame %1 failed hash check", frame);
1028                         ok = false;
1029                 }
1030         }
1031
1032         return ok;
1033 }