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