2 Copyright (C) 2012-2020 Carl Hetherington <cth@carlh.net>
4 This file is part of DCP-o-matic.
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.
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.
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/>.
22 #include "audio_buffers.h"
23 #include "compose.hpp"
25 #include "constants.h"
27 #include "dcpomatic_log.h"
30 #include "film_util.h"
31 #include "frame_info.h"
33 #include "image_png.h"
36 #include "reel_writer.h"
37 #include <dcp/atmos_asset.h>
38 #include <dcp/atmos_asset_writer.h>
39 #include <dcp/certificate_chain.h>
42 #include <dcp/filesystem.h>
43 #include <dcp/interop_subtitle_asset.h>
44 #include <dcp/mono_j2k_picture_asset.h>
45 #include <dcp/raw_convert.h>
47 #include <dcp/reel_atmos_asset.h>
48 #include <dcp/reel_interop_closed_caption_asset.h>
49 #include <dcp/reel_interop_subtitle_asset.h>
50 #include <dcp/reel_markers_asset.h>
51 #include <dcp/reel_mono_picture_asset.h>
52 #include <dcp/reel_smpte_closed_caption_asset.h>
53 #include <dcp/reel_smpte_subtitle_asset.h>
54 #include <dcp/reel_sound_asset.h>
55 #include <dcp/reel_stereo_picture_asset.h>
56 #include <dcp/smpte_subtitle_asset.h>
57 #include <dcp/sound_asset.h>
58 #include <dcp/sound_asset_writer.h>
59 #include <dcp/stereo_j2k_picture_asset.h>
60 #include <dcp/subtitle_image.h>
65 using std::dynamic_pointer_cast;
68 using std::make_shared;
71 using std::shared_ptr;
75 using boost::optional;
76 #if BOOST_VERSION >= 106100
77 using namespace boost::placeholders;
81 using dcp::raw_convert;
82 using namespace dcpomatic;
85 static dcp::MXFMetadata
88 dcp::MXFMetadata meta;
89 auto config = Config::instance();
90 if (!config->dcp_company_name().empty()) {
91 meta.company_name = config->dcp_company_name ();
93 if (!config->dcp_product_name().empty()) {
94 meta.product_name = config->dcp_product_name ();
96 if (!config->dcp_product_version().empty()) {
97 meta.product_version = config->dcp_product_version ();
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.
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
111 : WeakConstFilm (weak_film)
113 , _reel_index (reel_index)
114 , _reel_count (reel_count)
115 , _content_summary (film()->content_summary(period))
117 , _text_only (text_only)
118 , _font_metrics(film()->frame_size().height)
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.
125 auto const standard = film()->interop() ? dcp::Standard::INTEROP : dcp::Standard::SMPTE;
127 boost::filesystem::path const asset =
128 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
130 _first_nonexistent_frame = check_existing_picture_asset (asset);
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
138 if (dcp::filesystem::exists(asset) && dcp::filesystem::hard_link_count(asset) > 1) {
140 job->sub (_("Copying old video file"));
141 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
143 dcp::filesystem::copy_file(asset, asset.string() + ".tmp");
145 dcp::filesystem::remove(asset);
146 dcp::filesystem::rename(asset.string() + ".tmp", asset);
150 if (film()->three_d()) {
151 _picture_asset = std::make_shared<dcp::StereoJ2KPictureAsset>(dcp::Fraction(film()->video_frame_rate(), 1), standard);
153 _picture_asset = std::make_shared<dcp::MonoJ2KPictureAsset>(dcp::Fraction(film()->video_frame_rate(), 1), standard);
156 _picture_asset->set_size (film()->frame_size());
157 _picture_asset->set_metadata (mxf_metadata());
159 if (film()->encrypted()) {
160 _picture_asset->set_key (film()->key());
161 _picture_asset->set_context_id (film()->context_id());
164 _picture_asset->set_file (asset);
165 _picture_asset_writer = _picture_asset->start_write(asset, _first_nonexistent_frame > 0 ? dcp::Behaviour::OVERWRITE_EXISTING : dcp::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::StereoJ2KPictureAsset>(asset);
172 _picture_asset = make_shared<dcp::MonoJ2KPictureAsset>(asset);
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 film()->audio_channels(),
182 lang ? *lang : dcp::LanguageTag("en-US"),
186 _sound_asset->set_metadata (mxf_metadata());
188 if (film()->encrypted()) {
189 _sound_asset->set_key (film()->key());
192 DCPOMATIC_ASSERT (film()->directory());
194 std::vector<dcp::Channel> extra_active_channels;
195 for (auto channel: std::vector<dcp::Channel>{dcp::Channel::HI, dcp::Channel::VI, dcp::Channel::BSL, dcp::Channel::BSR}) {
196 if (channel_is_mapped(film(), channel)) {
197 extra_active_channels.push_back(channel);
201 /* Write the sound asset into the film directory so that we leave the creation
202 of the DCP directory until the last minute.
204 _sound_asset_writer = _sound_asset->start_write (
205 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
206 extra_active_channels,
207 film()->contains_atmos_content() ? dcp::SoundAsset::AtmosSync::ENABLED : dcp::SoundAsset::AtmosSync::DISABLED,
208 film()->limit_to_smpte_bv20() ? dcp::SoundAsset::MCASubDescriptors::DISABLED : dcp::SoundAsset::MCASubDescriptors::ENABLED
212 _default_font = dcp::ArrayData(default_font_file());
217 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
219 auto job = _job.lock ();
222 job->sub (_("Checking existing image data"));
225 /* Try to open the existing asset */
226 dcp::File asset_file(asset, "rb");
228 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
231 LOG_GENERAL ("Opened existing asset at %1", asset.string());
234 shared_ptr<InfoFileHandle> info_file;
237 info_file = film()->info_file_handle (_period, true);
238 } catch (OpenFileError &) {
239 LOG_GENERAL_NC ("Could not open film info file");
243 /* Offset of the last dcp::FrameInfo in the info file */
244 int const n = (dcp::filesystem::file_size(info_file->get().path()) / J2KFrameInfo::size_on_disk()) - 1;
245 LOG_GENERAL("The last FI is %1; info file is %2, info size %3", n, dcp::filesystem::file_size(info_file->get().path()), J2KFrameInfo::size_on_disk())
247 Frame first_nonexistent_frame;
248 if (film()->three_d()) {
249 /* Start looking at the last left frame */
250 first_nonexistent_frame = n / 2;
252 first_nonexistent_frame = n;
255 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistent_frame) && first_nonexistent_frame > 0) {
256 --first_nonexistent_frame;
259 if (!film()->three_d() && first_nonexistent_frame > 0) {
260 /* If we are doing 3D we might have found a good L frame with no R, so only
261 do this if we're in 2D and we've just found a good B(oth) frame.
263 ++first_nonexistent_frame;
266 LOG_GENERAL ("Proceeding with first nonexistent frame %1", first_nonexistent_frame);
268 return first_nonexistent_frame;
273 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
275 if (!_picture_asset_writer) {
276 /* We're not writing any data */
280 auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(encoded->data(), encoded->size()));
281 fin.write(film()->info_file_handle(_period, false), frame, eyes);
282 _last_written[eyes] = encoded;
287 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
290 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
291 if (film()->encrypted()) {
292 _atmos_asset->set_key(film()->key());
294 _atmos_asset_writer = _atmos_asset->start_write (
295 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
298 _atmos_asset_writer->write (atmos);
303 ReelWriter::fake_write (int size)
305 if (!_picture_asset_writer) {
306 /* We're not writing any data */
310 _picture_asset_writer->fake_write (size);
315 ReelWriter::repeat_write (Frame frame, Eyes eyes)
317 if (!_picture_asset_writer) {
318 /* We're not writing any data */
322 auto fin = J2KFrameInfo(_j2k_picture_asset_writer->write(_last_written[eyes]->data(), _last_written[eyes]->size()));
323 fin.write(film()->info_file_handle(_period, false), frame, eyes);
328 ReelWriter::finish (boost::filesystem::path output_dcp)
330 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
331 /* Nothing was written to the picture asset */
332 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
333 _picture_asset.reset ();
336 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
337 /* Nothing was written to the sound asset */
338 _sound_asset.reset ();
341 /* Hard-link any video asset file into the DCP */
342 if (_picture_asset) {
343 DCPOMATIC_ASSERT (_picture_asset->file());
344 boost::filesystem::path video_from = _picture_asset->file().get();
345 boost::filesystem::path video_to = output_dcp;
346 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
347 /* There may be an existing "to" file if we are recreating a DCP in the same place without
350 boost::system::error_code ec;
351 dcp::filesystem::remove(video_to, ec);
353 dcp::filesystem::create_hard_link(video_from, video_to, ec);
355 LOG_WARNING("Hard-link failed (%1); copying instead", error_details(ec));
356 auto job = _job.lock ();
358 job->sub (_("Copying video file into DCP"));
360 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
361 } catch (exception& e) {
362 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
363 throw FileError (e.what(), video_from);
366 dcp::filesystem::copy_file(video_from, video_to, ec);
368 LOG_ERROR("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), error_details(ec));
369 throw FileError (ec.message(), video_from);
374 _picture_asset->set_file (video_to);
377 /* Move the audio asset into the DCP */
379 boost::filesystem::path audio_to = output_dcp;
380 auto const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
383 boost::system::error_code ec;
384 dcp::filesystem::rename(film()->file(aaf), audio_to, ec);
387 String::compose(_("could not move audio asset into the DCP (%1)"), error_details(ec)), aaf
391 _sound_asset->set_file (audio_to);
395 _atmos_asset_writer->finalize ();
396 boost::filesystem::path atmos_to = output_dcp;
397 auto const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
400 boost::system::error_code ec;
401 dcp::filesystem::rename(film()->file(aaf), atmos_to, ec);
404 String::compose(_("could not move atmos asset into the DCP (%1)"), error_details(ec)), aaf
408 _atmos_asset->set_file (atmos_to);
413 /** Try to make a ReelAsset for a subtitles or closed captions in a given period in the DCP.
414 * A SubtitleAsset can be provided, or we will use one from @ref refs if not.
416 template <class Interop, class SMPTE, class Result>
419 shared_ptr<dcp::SubtitleAsset> asset,
420 int64_t picture_duration,
421 shared_ptr<dcp::Reel> reel,
424 optional<string> content_summary,
425 list<ReferencedReelAsset> const & refs,
426 shared_ptr<const Film> film,
427 DCPTimePeriod period,
428 boost::filesystem::path output_dcp,
432 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
434 shared_ptr<Result> reel_asset;
437 if (auto interop = dynamic_pointer_cast<dcp::InteropSubtitleAsset>(asset)) {
438 auto directory = output_dcp / interop->id ();
439 dcp::filesystem::create_directories(directory);
440 interop->write (directory / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".xml"));
441 reel_asset = make_shared<Interop> (
443 dcp::Fraction(film->video_frame_rate(), 1),
447 } else if (auto smpte = dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)) {
448 /* All our assets should be the same length; use the picture asset length here
449 as a reference to set the subtitle one. We'll use the duration rather than
450 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
451 just interested in its presentation length.
453 smpte->set_intrinsic_duration(picture_duration);
455 output_dcp / subtitle_asset_filename(asset, reel_index, reel_count, content_summary, ".mxf")
457 reel_asset = make_shared<SMPTE> (
459 dcp::Fraction(film->video_frame_rate(), 1),
466 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
468 auto k = dynamic_pointer_cast<Result> (j.asset);
469 if (k && j.period == period) {
471 /* If we have a hash for this asset in the CPL, assume that it is correct */
473 k->asset_ref()->set_hash (k->hash().get());
480 if (!text_only && reel_asset->actual_duration() != period_duration) {
481 throw ProgrammingError (
483 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
486 reel->add (reel_asset);
493 shared_ptr<dcp::ReelPictureAsset>
494 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
496 shared_ptr<dcp::ReelPictureAsset> reel_asset;
498 if (_picture_asset) {
499 /* We have made a picture asset of our own. Put it into the reel */
500 auto mono = dynamic_pointer_cast<dcp::MonoJ2KPictureAsset>(_picture_asset);
502 reel_asset = make_shared<dcp::ReelMonoPictureAsset>(mono, 0);
505 auto stereo = dynamic_pointer_cast<dcp::StereoJ2KPictureAsset>(_picture_asset);
507 reel_asset = make_shared<dcp::ReelStereoPictureAsset>(stereo, 0);
510 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
511 /* We don't have a picture asset of our own; hopefully we have one to reference */
513 auto k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
515 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
517 if (k && j.period == _period) {
523 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
525 DCPOMATIC_ASSERT (reel_asset);
526 if (reel_asset->duration() != period_duration) {
527 throw ProgrammingError (
529 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
532 reel->add (reel_asset);
534 /* If we have a hash for this asset in the CPL, assume that it is correct */
535 if (reel_asset->hash()) {
536 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
544 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
546 shared_ptr<dcp::ReelSoundAsset> reel_asset;
549 /* We have made a sound asset of our own. Put it into the reel */
550 reel_asset = make_shared<dcp::ReelSoundAsset>(_sound_asset, 0);
552 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
553 /* We don't have a sound asset of our own; hopefully we have one to reference */
555 auto k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
557 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
559 if (k && j.period == _period) {
561 /* If we have a hash for this asset in the CPL, assume that it is correct */
563 k->asset_ref()->set_hash (k->hash().get());
569 auto const period_duration = _period.duration().frames_round(film()->video_frame_rate());
571 DCPOMATIC_ASSERT (reel_asset);
572 if (reel_asset->actual_duration() != period_duration) {
574 "Reel sound asset has length %1 but reel period is %2",
575 reel_asset->actual_duration(),
578 if (reel_asset->actual_duration() != period_duration) {
579 throw ProgrammingError (
581 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
586 reel->add (reel_asset);
591 ReelWriter::create_reel_text (
592 shared_ptr<dcp::Reel> reel,
593 list<ReferencedReelAsset> const & refs,
595 boost::filesystem::path output_dcp,
596 bool ensure_subtitles,
597 set<DCPTextTrack> ensure_closed_captions
600 auto subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
601 _subtitle_asset, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
604 if (!subtitle && ensure_subtitles) {
605 /* We had no subtitle asset, but we've been asked to make sure there is one */
606 subtitle = maybe_add_text<dcp::ReelInteropSubtitleAsset, dcp::ReelSMPTESubtitleAsset, dcp::ReelSubtitleAsset> (
607 empty_text_asset(TextType::OPEN_SUBTITLE, optional<DCPTextTrack>(), true),
622 /* We have a subtitle asset that we either made or are referencing */
623 if (auto main_language = film()->subtitle_languages().first) {
624 subtitle->set_language (*main_language);
628 for (auto const& i: _closed_caption_assets) {
629 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
630 i.second, duration, reel, _reel_index, _reel_count, _content_summary, refs, film(), _period, output_dcp, _text_only
632 DCPOMATIC_ASSERT (a);
633 a->set_annotation_text (i.first.name);
634 if (i.first.language) {
635 a->set_language (i.first.language.get());
638 ensure_closed_captions.erase (i.first);
641 /* Make empty tracks for anything we've been asked to ensure but that we haven't added */
642 for (auto i: ensure_closed_captions) {
643 auto a = maybe_add_text<dcp::ReelInteropClosedCaptionAsset, dcp::ReelSMPTEClosedCaptionAsset, dcp::ReelClosedCaptionAsset> (
644 empty_text_asset(TextType::CLOSED_CAPTION, i, true),
656 DCPOMATIC_ASSERT (a);
657 a->set_annotation_text (i.name);
659 a->set_language (i.language.get());
666 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
668 auto markers = film()->markers();
669 film()->add_ffoc_lfoc(markers);
670 Film::Markers reel_markers;
671 for (auto const& i: markers) {
672 if (_period.contains(i.second)) {
673 reel_markers[i.first] = i.second;
677 if (!reel_markers.empty ()) {
678 auto ma = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(film()->video_frame_rate(), 1), reel->duration());
679 for (auto const& i: reel_markers) {
680 DCPTime relative = i.second - _period.from;
681 auto hmsf = relative.split (film()->video_frame_rate());
682 ma->set (i.first, dcp::Time(hmsf.h, hmsf.m, hmsf.s, hmsf.f, film()->video_frame_rate()));
689 /** @param ensure_subtitles true to make sure the reel has a subtitle asset.
690 * @param ensure_closed_captions make sure the reel has these closed caption tracks.
692 shared_ptr<dcp::Reel>
693 ReelWriter::create_reel (
694 list<ReferencedReelAsset> const & refs,
695 boost::filesystem::path output_dcp,
696 bool ensure_subtitles,
697 set<DCPTextTrack> ensure_closed_captions
700 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
702 auto reel = make_shared<dcp::Reel>();
704 /* This is a bit of a hack; in the strange `_text_only' mode we have no picture, so we don't know
705 * how long the subtitle / CCAP assets should be. However, since we're only writing them to see
706 * how big they are, we don't care about that.
708 int64_t duration = 0;
710 auto reel_picture_asset = create_reel_picture (reel, refs);
711 duration = reel_picture_asset->actual_duration ();
712 create_reel_sound (reel, refs);
713 if (!film()->interop()) {
714 create_reel_markers(reel);
718 create_reel_text(reel, refs, duration, output_dcp, ensure_subtitles, ensure_closed_captions);
721 reel->add (make_shared<dcp::ReelAtmosAsset>(_atmos_asset, 0));
728 /** @param set_progress Method to call with progress; first parameter is the number of bytes
729 * done, second parameter is the number of bytes in total.
732 ReelWriter::calculate_digests(std::function<void (int64_t, int64_t)> set_progress)
735 vector<shared_ptr<const dcp::Asset>> assets;
737 if (_picture_asset) {
738 assets.push_back(_picture_asset);
741 assets.push_back(_sound_asset);
744 assets.push_back(_atmos_asset);
747 int64_t total_size = 0;
748 for (auto asset: assets) {
749 total_size += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
752 int64_t total_done = 0;
753 for (auto asset: assets) {
754 asset->hash([&total_done, total_size, set_progress](int64_t done, int64_t) {
755 set_progress(total_done + done, total_size);
757 total_done += asset->file() ? boost::filesystem::file_size(*asset->file()) : 0;
760 } catch (boost::thread_interrupted) {
761 /* set_progress contains an interruption_point, so any of these methods
762 * may throw thread_interrupted, at which point we just give up.
768 ReelWriter::start () const
770 return _period.from.frames_floor (film()->video_frame_rate());
775 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
777 if (!_sound_asset_writer) {
781 DCPOMATIC_ASSERT (audio);
782 _sound_asset_writer->write(audio->data(), audio->channels(), audio->frames());
786 shared_ptr<dcp::SubtitleAsset>
787 ReelWriter::empty_text_asset (TextType type, optional<DCPTextTrack> track, bool with_dummy) const
789 shared_ptr<dcp::SubtitleAsset> asset;
790 optional<string> font;
792 auto lang = film()->subtitle_languages();
793 if (film()->interop()) {
794 auto s = make_shared<dcp::InteropSubtitleAsset>();
795 s->set_movie_title (film()->name());
796 if (type == TextType::OPEN_SUBTITLE) {
797 s->set_language (lang.first ? lang.first->to_string() : "Unknown");
798 } else if (track->language) {
799 s->set_language (track->language->to_string());
801 s->set_reel_number (raw_convert<string> (_reel_index + 1));
804 auto s = make_shared<dcp::SMPTESubtitleAsset>();
805 s->set_content_title_text (film()->name());
806 s->set_metadata (mxf_metadata());
807 if (type == TextType::OPEN_SUBTITLE && lang.first) {
808 s->set_language (*lang.first);
809 } else if (track && track->language) {
810 s->set_language (dcp::LanguageTag(track->language->to_string()));
812 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
813 s->set_reel_number (_reel_index + 1);
814 s->set_time_code_rate (film()->video_frame_rate());
815 s->set_start_time (dcp::Time ());
816 if (film()->encrypted()) {
817 s->set_key (film()->key());
824 std::make_shared<dcp::SubtitleString>(
832 dcp::Time(0, 0, 0, 0, 24),
833 dcp::Time(0, 0, 1, 0, 24),
846 std::vector<dcp::Ruby>()
850 if (!film()->interop()) {
851 /* We must have a LoadFont since we have a Text */
853 asset->ensure_font(*font, _default_font);
862 ReelWriter::convert_vertical_position(StringText const& subtitle, dcp::SubtitleStandard to) const
864 if (dcp::uses_baseline(subtitle.valign_standard) == dcp::uses_baseline(to)) {
865 /* The from and to standards use the same alignment reference */
866 return subtitle.v_position();
869 auto const baseline_to_bottom = _font_metrics.baseline_to_bottom(subtitle);
870 auto const height = _font_metrics.height(subtitle);
872 float correction = 0;
873 switch (subtitle.v_align()) {
874 case dcp::VAlign::TOP:
875 correction = height - baseline_to_bottom;
877 case dcp::VAlign::CENTER:
878 correction = (height / 2) - baseline_to_bottom;
880 case dcp::VAlign::BOTTOM:
881 correction = baseline_to_bottom;
885 return subtitle.v_position() + (dcp::uses_bounding_box(subtitle.valign_standard) ? correction : -correction);
890 ReelWriter::write(PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period, FontIdMap const& fonts, shared_ptr<dcpomatic::Font> chosen_interop_font)
892 shared_ptr<dcp::SubtitleAsset> asset;
895 case TextType::OPEN_SUBTITLE:
896 asset = _subtitle_asset;
898 case TextType::CLOSED_CAPTION:
899 DCPOMATIC_ASSERT (track);
900 asset = _closed_caption_assets[*track];
903 DCPOMATIC_ASSERT (false);
907 asset = empty_text_asset (type, track, false);
911 case TextType::OPEN_SUBTITLE:
912 _subtitle_asset = asset;
914 case TextType::CLOSED_CAPTION:
915 DCPOMATIC_ASSERT (track);
916 _closed_caption_assets[*track] = asset;
919 DCPOMATIC_ASSERT (false);
922 /* timecode rate for subtitles we emit; we might as well stick to ms accuracy here, I think */
923 auto const tcr = 1000;
925 for (auto i: subs.string) {
926 i.set_in (dcp::Time(period.from.seconds() - _period.from.seconds(), tcr));
927 i.set_out (dcp::Time(period.to.seconds() - _period.from.seconds(), tcr));
928 i.set_v_position(convert_vertical_position(i, film()->interop() ? dcp::SubtitleStandard::INTEROP : dcp::SubtitleStandard::SMPTE_2014));
929 auto sub = make_shared<dcp::SubtitleString>(i);
930 /* i.font is a shared_ptr<Font> which uniquely identifies the font we want,
931 * though if we are Interop we can only have one font, so we'll use the chosen
934 auto font = film()->interop() ? chosen_interop_font : i.font;
935 /* We can get the corresponding ID from fonts */
936 auto const font_id_to_use = fonts.get(font);
937 /* Give this subtitle the correct font ID */
938 sub->set_font(font_id_to_use);
940 /* Make sure the asset LoadFonts the font we just asked for */
941 asset->ensure_font(font_id_to_use, font->data().get_value_or(_default_font));
944 for (auto i: subs.bitmap) {
946 make_shared<dcp::SubtitleImage>(
947 image_as_png(i.image),
948 dcp::Time(period.from.seconds() - _period.from.seconds(), tcr),
949 dcp::Time(period.to.seconds() - _period.from.seconds(), tcr),
950 i.rectangle.x, dcp::HAlign::LEFT, i.rectangle.y, dcp::VAlign::TOP, 0,
951 dcp::Time(), dcp::Time()
959 ReelWriter::existing_picture_frame_ok (dcp::File& asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
961 LOG_GENERAL ("Checking existing picture frame %1", frame);
963 /* Read the data from the info file; for 3D we just check the left
964 frames until we find a good one.
966 auto const info = J2KFrameInfo(info_file, frame, film()->three_d() ? Eyes::LEFT : Eyes::BOTH);
970 /* Read the data from the asset and hash it */
971 asset_file.seek(info.offset, SEEK_SET);
972 ArrayData data (info.size);
973 size_t const read = asset_file.read(data.data(), 1, data.size());
974 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
975 if (read != static_cast<size_t> (data.size ())) {
976 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
980 digester.add (data.data(), data.size());
981 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
982 if (digester.get() != info.hash) {
983 LOG_GENERAL ("Existing frame %1 failed hash check", frame);