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/>.
21 #include "reel_writer.h"
26 #include "dcpomatic_log.h"
29 #include "compose.hpp"
31 #include "audio_buffers.h"
33 #include <dcp/atmos_asset.h>
34 #include <dcp/atmos_asset_writer.h>
35 #include <dcp/mono_picture_asset.h>
36 #include <dcp/stereo_picture_asset.h>
37 #include <dcp/sound_asset.h>
38 #include <dcp/sound_asset_writer.h>
40 #include <dcp/reel_atmos_asset.h>
41 #include <dcp/reel_mono_picture_asset.h>
42 #include <dcp/reel_stereo_picture_asset.h>
43 #include <dcp/reel_sound_asset.h>
44 #include <dcp/reel_subtitle_asset.h>
45 #include <dcp/reel_closed_caption_asset.h>
46 #include <dcp/reel_markers_asset.h>
49 #include <dcp/certificate_chain.h>
50 #include <dcp/interop_subtitle_asset.h>
51 #include <dcp/smpte_subtitle_asset.h>
52 #include <dcp/raw_convert.h>
53 #include <dcp/subtitle_image.h>
54 #include <boost/foreach.hpp>
64 using boost::shared_ptr;
65 using boost::optional;
66 using boost::dynamic_pointer_cast;
67 #if BOOST_VERSION >= 106100
68 using namespace boost::placeholders;
70 using boost::weak_ptr;
73 using dcp::raw_convert;
74 using namespace dcpomatic;
76 int const ReelWriter::_info_size = 48;
78 static dcp::MXFMetadata
81 dcp::MXFMetadata meta;
82 Config* config = Config::instance();
83 if (!config->dcp_company_name().empty()) {
84 meta.company_name = config->dcp_company_name ();
86 if (!config->dcp_product_name().empty()) {
87 meta.product_name = config->dcp_product_name ();
89 if (!config->dcp_product_version().empty()) {
90 meta.product_version = config->dcp_product_version ();
95 /** @param job Related job, or 0 */
96 ReelWriter::ReelWriter (
97 weak_ptr<const Film> weak_film, DCPTimePeriod period, shared_ptr<Job> job, int reel_index, int reel_count
99 : WeakConstFilm (weak_film)
101 , _reel_index (reel_index)
102 , _reel_count (reel_count)
103 , _content_summary (film()->content_summary(period))
106 /* Create or find our picture asset in a subdirectory, named
107 according to those film's parameters which affect the video
108 output. We will hard-link it into the DCP later.
111 dcp::Standard const standard = film()->interop() ? dcp::INTEROP : dcp::SMPTE;
113 boost::filesystem::path const asset =
114 film()->internal_video_asset_dir() / film()->internal_video_asset_filename(_period);
116 _first_nonexistant_frame = check_existing_picture_asset (asset);
118 if (_first_nonexistant_frame < period.duration().frames_round(film()->video_frame_rate())) {
119 /* We do not have a complete picture asset. If there is an
120 existing asset, break any hard links to it as we are about
121 to change its contents (if only by changing the IDs); see
124 if (boost::filesystem::exists(asset) && boost::filesystem::hard_link_count(asset) > 1) {
126 job->sub (_("Copying old video file"));
127 copy_in_bits (asset, asset.string() + ".tmp", bind(&Job::set_progress, job.get(), _1, false));
129 boost::filesystem::copy_file (asset, asset.string() + ".tmp");
131 boost::filesystem::remove (asset);
132 boost::filesystem::rename (asset.string() + ".tmp", asset);
136 if (film()->three_d()) {
137 _picture_asset.reset (new dcp::StereoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
139 _picture_asset.reset (new dcp::MonoPictureAsset(dcp::Fraction(film()->video_frame_rate(), 1), standard));
142 _picture_asset->set_size (film()->frame_size());
143 _picture_asset->set_metadata (mxf_metadata());
145 if (film()->encrypted()) {
146 _picture_asset->set_key (film()->key());
147 _picture_asset->set_context_id (film()->context_id());
150 _picture_asset->set_file (asset);
151 _picture_asset_writer = _picture_asset->start_write (asset, _first_nonexistant_frame > 0);
153 /* We already have a complete picture asset that we can just re-use */
154 /* XXX: what about if the encryption key changes? */
155 if (film()->three_d()) {
156 _picture_asset.reset (new dcp::StereoPictureAsset(asset));
158 _picture_asset.reset (new dcp::MonoPictureAsset(asset));
162 if (film()->audio_channels()) {
164 new dcp::SoundAsset (dcp::Fraction(film()->video_frame_rate(), 1), film()->audio_frame_rate(), film()->audio_channels(), film()->audio_language(), standard)
167 _sound_asset->set_metadata (mxf_metadata());
169 if (film()->encrypted()) {
170 _sound_asset->set_key (film()->key());
173 DCPOMATIC_ASSERT (film()->directory());
175 vector<dcp::Channel> active;
176 BOOST_FOREACH (int i, film()->mapped_audio_channels()) {
177 active.push_back (static_cast<dcp::Channel>(i));
180 /* Write the sound asset into the film directory so that we leave the creation
181 of the DCP directory until the last minute.
183 _sound_asset_writer = _sound_asset->start_write (
184 film()->directory().get() / audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary),
186 film()->contains_atmos_content()
191 /** @param frame reel-relative frame */
193 ReelWriter::write_frame_info (Frame frame, Eyes eyes, dcp::FrameInfo info) const
195 shared_ptr<InfoFileHandle> handle = film()->info_file_handle(_period, false);
196 dcpomatic_fseek (handle->get(), frame_info_position(frame, eyes), SEEK_SET);
197 checked_fwrite (&info.offset, sizeof(info.offset), handle->get(), handle->file());
198 checked_fwrite (&info.size, sizeof (info.size), handle->get(), handle->file());
199 checked_fwrite (info.hash.c_str(), info.hash.size(), handle->get(), handle->file());
203 ReelWriter::read_frame_info (shared_ptr<InfoFileHandle> info, Frame frame, Eyes eyes) const
205 dcp::FrameInfo frame_info;
206 dcpomatic_fseek (info->get(), frame_info_position(frame, eyes), SEEK_SET);
207 checked_fread (&frame_info.offset, sizeof(frame_info.offset), info->get(), info->file());
208 checked_fread (&frame_info.size, sizeof(frame_info.size), info->get(), info->file());
210 char hash_buffer[33];
211 checked_fread (hash_buffer, 32, info->get(), info->file());
212 hash_buffer[32] = '\0';
213 frame_info.hash = hash_buffer;
219 ReelWriter::frame_info_position (Frame frame, Eyes eyes) const
223 return frame * _info_size;
225 return frame * _info_size * 2;
227 return frame * _info_size * 2 + _info_size;
229 DCPOMATIC_ASSERT (false);
232 DCPOMATIC_ASSERT (false);
236 ReelWriter::check_existing_picture_asset (boost::filesystem::path asset)
238 shared_ptr<Job> job = _job.lock ();
241 job->sub (_("Checking existing image data"));
244 /* Try to open the existing asset */
245 FILE* asset_file = fopen_boost (asset, "rb");
247 LOG_GENERAL ("Could not open existing asset at %1 (errno=%2)", asset.string(), errno);
250 LOG_GENERAL ("Opened existing asset at %1", asset.string());
253 shared_ptr<InfoFileHandle> info_file;
256 info_file = film()->info_file_handle (_period, true);
257 } catch (OpenFileError &) {
258 LOG_GENERAL_NC ("Could not open film info file");
263 /* Offset of the last dcp::FrameInfo in the info file */
264 int const n = (boost::filesystem::file_size(info_file->file()) / _info_size) - 1;
265 LOG_GENERAL ("The last FI is %1; info file is %2, info size %3", n, boost::filesystem::file_size(info_file->file()), _info_size);
267 Frame first_nonexistant_frame;
268 if (film()->three_d()) {
269 /* Start looking at the last left frame */
270 first_nonexistant_frame = n / 2;
272 first_nonexistant_frame = n;
275 while (!existing_picture_frame_ok(asset_file, info_file, first_nonexistant_frame) && first_nonexistant_frame > 0) {
276 --first_nonexistant_frame;
279 if (!film()->three_d() && first_nonexistant_frame > 0) {
280 /* If we are doing 3D we might have found a good L frame with no R, so only
281 do this if we're in 2D and we've just found a good B(oth) frame.
283 ++first_nonexistant_frame;
286 LOG_GENERAL ("Proceeding with first nonexistant frame %1", first_nonexistant_frame);
290 return first_nonexistant_frame;
294 ReelWriter::write (shared_ptr<const Data> encoded, Frame frame, Eyes eyes)
296 if (!_picture_asset_writer) {
297 /* We're not writing any data */
301 dcp::FrameInfo fin = _picture_asset_writer->write (encoded->data(), encoded->size());
302 write_frame_info (frame, eyes, fin);
303 _last_written[eyes] = encoded;
308 ReelWriter::write (shared_ptr<const dcp::AtmosFrame> atmos, AtmosMetadata metadata)
311 _atmos_asset = metadata.create (dcp::Fraction(film()->video_frame_rate(), 1));
312 if (film()->encrypted()) {
313 _atmos_asset->set_key(film()->key());
315 _atmos_asset_writer = _atmos_asset->start_write (
316 film()->directory().get() / atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary)
319 _atmos_asset_writer->write (atmos);
324 ReelWriter::fake_write (int size)
326 if (!_picture_asset_writer) {
327 /* We're not writing any data */
331 _picture_asset_writer->fake_write (size);
335 ReelWriter::repeat_write (Frame frame, Eyes eyes)
337 if (!_picture_asset_writer) {
338 /* We're not writing any data */
342 dcp::FrameInfo fin = _picture_asset_writer->write (
343 _last_written[eyes]->data(),
344 _last_written[eyes]->size()
346 write_frame_info (frame, eyes, fin);
350 ReelWriter::finish (boost::filesystem::path output_dcp)
352 if (_picture_asset_writer && !_picture_asset_writer->finalize ()) {
353 /* Nothing was written to the picture asset */
354 LOG_GENERAL ("Nothing was written to reel %1 of %2", _reel_index, _reel_count);
355 _picture_asset.reset ();
358 if (_sound_asset_writer && !_sound_asset_writer->finalize ()) {
359 /* Nothing was written to the sound asset */
360 _sound_asset.reset ();
363 /* Hard-link any video asset file into the DCP */
364 if (_picture_asset) {
365 DCPOMATIC_ASSERT (_picture_asset->file());
366 boost::filesystem::path video_from = _picture_asset->file().get();
367 boost::filesystem::path video_to = output_dcp;
368 video_to /= video_asset_filename (_picture_asset, _reel_index, _reel_count, _content_summary);
369 /* There may be an existing "to" file if we are recreating a DCP in the same place without
372 boost::system::error_code ec;
373 boost::filesystem::remove (video_to, ec);
375 boost::filesystem::create_hard_link (video_from, video_to, ec);
377 LOG_WARNING_NC ("Hard-link failed; copying instead");
378 shared_ptr<Job> job = _job.lock ();
380 job->sub (_("Copying video file into DCP"));
382 copy_in_bits (video_from, video_to, bind(&Job::set_progress, job.get(), _1, false));
383 } catch (exception& e) {
384 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), e.what());
385 throw FileError (e.what(), video_from);
388 boost::filesystem::copy_file (video_from, video_to, ec);
390 LOG_ERROR ("Failed to copy video file from %1 to %2 (%3)", video_from.string(), video_to.string(), ec.message());
391 throw FileError (ec.message(), video_from);
396 _picture_asset->set_file (video_to);
399 /* Move the audio asset into the DCP */
401 boost::filesystem::path audio_to = output_dcp;
402 string const aaf = audio_asset_filename (_sound_asset, _reel_index, _reel_count, _content_summary);
405 boost::system::error_code ec;
406 boost::filesystem::rename (film()->file(aaf), audio_to, ec);
409 String::compose (_("could not move audio asset into the DCP (%1)"), ec.value ()), aaf
413 _sound_asset->set_file (audio_to);
417 _atmos_asset_writer->finalize ();
418 boost::filesystem::path atmos_to = output_dcp;
419 string const aaf = atmos_asset_filename (_atmos_asset, _reel_index, _reel_count, _content_summary);
422 boost::system::error_code ec;
423 boost::filesystem::rename (film()->file(aaf), atmos_to, ec);
426 String::compose (_("could not move atmos asset into the DCP (%1)"), ec.value ()), aaf
430 _atmos_asset->set_file (atmos_to);
437 shared_ptr<dcp::SubtitleAsset> asset,
438 int64_t picture_duration,
439 shared_ptr<dcp::Reel> reel,
440 list<ReferencedReelAsset> const & refs,
441 list<shared_ptr<Font> > const & fonts,
442 shared_ptr<const Film> film,
443 DCPTimePeriod period,
444 boost::filesystem::path output_dcp
447 Frame const period_duration = period.duration().frames_round(film->video_frame_rate());
449 shared_ptr<T> reel_asset;
452 /* Add the font to the subtitle content */
453 BOOST_FOREACH (shared_ptr<Font> j, fonts) {
454 asset->add_font (j->id(), j->file().get_value_or(default_font_file()));
457 if (dynamic_pointer_cast<dcp::InteropSubtitleAsset> (asset)) {
458 boost::filesystem::path directory = output_dcp / asset->id ();
459 boost::filesystem::create_directories (directory);
460 asset->write (directory / ("sub_" + asset->id() + ".xml"));
462 /* All our assets should be the same length; use the picture asset length here
463 as a reference to set the subtitle one. We'll use the duration rather than
464 the intrinsic duration; we don't care if the picture asset has been trimmed, we're
465 just interested in its presentation length.
467 dynamic_pointer_cast<dcp::SMPTESubtitleAsset>(asset)->set_intrinsic_duration (picture_duration);
470 output_dcp / ("sub_" + asset->id() + ".mxf")
477 dcp::Fraction (film->video_frame_rate(), 1),
483 /* We don't have a subtitle asset of our own; hopefully we have one to reference */
484 BOOST_FOREACH (ReferencedReelAsset j, refs) {
485 shared_ptr<T> k = dynamic_pointer_cast<T> (j.asset);
486 if (k && j.period == period) {
488 /* If we have a hash for this asset in the CPL, assume that it is correct */
490 k->asset_ref()->set_hash (k->hash().get());
497 if (reel_asset->actual_duration() != period_duration) {
498 throw ProgrammingError (
500 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
503 reel->add (reel_asset);
510 shared_ptr<dcp::ReelPictureAsset>
511 ReelWriter::create_reel_picture (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
513 shared_ptr<dcp::ReelPictureAsset> reel_asset;
515 if (_picture_asset) {
516 /* We have made a picture asset of our own. Put it into the reel */
517 shared_ptr<dcp::MonoPictureAsset> mono = dynamic_pointer_cast<dcp::MonoPictureAsset> (_picture_asset);
519 reel_asset.reset (new dcp::ReelMonoPictureAsset (mono, 0));
522 shared_ptr<dcp::StereoPictureAsset> stereo = dynamic_pointer_cast<dcp::StereoPictureAsset> (_picture_asset);
524 reel_asset.reset (new dcp::ReelStereoPictureAsset (stereo, 0));
527 LOG_GENERAL ("no picture asset of our own; look through %1", refs.size());
528 /* We don't have a picture asset of our own; hopefully we have one to reference */
529 BOOST_FOREACH (ReferencedReelAsset j, refs) {
530 shared_ptr<dcp::ReelPictureAsset> k = dynamic_pointer_cast<dcp::ReelPictureAsset> (j.asset);
532 LOG_GENERAL ("candidate picture asset period is %1-%2", j.period.from.get(), j.period.to.get());
534 if (k && j.period == _period) {
540 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
542 DCPOMATIC_ASSERT (reel_asset);
543 if (reel_asset->duration() != period_duration) {
544 throw ProgrammingError (
546 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
549 reel->add (reel_asset);
551 /* If we have a hash for this asset in the CPL, assume that it is correct */
552 if (reel_asset->hash()) {
553 reel_asset->asset_ref()->set_hash (reel_asset->hash().get());
561 ReelWriter::create_reel_sound (shared_ptr<dcp::Reel> reel, list<ReferencedReelAsset> const & refs) const
563 shared_ptr<dcp::ReelSoundAsset> reel_asset;
566 /* We have made a sound asset of our own. Put it into the reel */
567 reel_asset.reset (new dcp::ReelSoundAsset(_sound_asset, 0));
569 LOG_GENERAL ("no sound asset of our own; look through %1", refs.size());
570 /* We don't have a sound asset of our own; hopefully we have one to reference */
571 BOOST_FOREACH (ReferencedReelAsset j, refs) {
572 shared_ptr<dcp::ReelSoundAsset> k = dynamic_pointer_cast<dcp::ReelSoundAsset> (j.asset);
574 LOG_GENERAL ("candidate sound asset period is %1-%2", j.period.from.get(), j.period.to.get());
576 if (k && j.period == _period) {
578 /* If we have a hash for this asset in the CPL, assume that it is correct */
580 k->asset_ref()->set_hash (k->hash().get());
586 Frame const period_duration = _period.duration().frames_round(film()->video_frame_rate());
588 DCPOMATIC_ASSERT (reel_asset);
589 if (reel_asset->actual_duration() != period_duration) {
591 "Reel sound asset has length %1 but reel period is %2",
592 reel_asset->actual_duration(),
595 if (reel_asset->actual_duration() != period_duration) {
596 throw ProgrammingError (
598 String::compose ("%1 vs %2", reel_asset->actual_duration(), period_duration)
603 reel->add (reel_asset);
608 ReelWriter::create_reel_text (
609 shared_ptr<dcp::Reel> reel,
610 list<ReferencedReelAsset> const & refs,
611 list<shared_ptr<Font> > const& fonts,
613 boost::filesystem::path output_dcp
616 shared_ptr<dcp::ReelSubtitleAsset> subtitle = maybe_add_text<dcp::ReelSubtitleAsset> (
617 _subtitle_asset, duration, reel, refs, fonts, film(), _period, output_dcp
619 if (subtitle && !film()->subtitle_languages().empty()) {
620 subtitle->set_language (film()->subtitle_languages().front());
623 for (map<DCPTextTrack, shared_ptr<dcp::SubtitleAsset> >::const_iterator i = _closed_caption_assets.begin(); i != _closed_caption_assets.end(); ++i) {
624 shared_ptr<dcp::ReelClosedCaptionAsset> a = maybe_add_text<dcp::ReelClosedCaptionAsset> (
625 i->second, duration, reel, refs, fonts, film(), _period, output_dcp
628 a->set_annotation_text (i->first.name);
629 if (!i->first.language.empty()) {
630 a->set_language (dcp::LanguageTag(i->first.language));
639 ReelWriter::create_reel_markers (shared_ptr<dcp::Reel> reel) const
641 Film::Markers markers = film()->markers();
642 film()->add_ffoc_lfoc(markers);
643 Film::Markers reel_markers;
644 for (Film::Markers::const_iterator i = markers.begin(); i != markers.end(); ++i) {
645 if (_period.contains(i->second)) {
646 reel_markers[i->first] = i->second;
650 if (!reel_markers.empty ()) {
651 shared_ptr<dcp::ReelMarkersAsset> ma (new dcp::ReelMarkersAsset(dcp::Fraction(film()->video_frame_rate(), 1), 0));
652 for (map<dcp::Marker, DCPTime>::const_iterator i = reel_markers.begin(); i != reel_markers.end(); ++i) {
654 DCPTime relative = i->second - _period.from;
655 relative.split (film()->video_frame_rate(), h, m, s, f);
656 ma->set (i->first, dcp::Time(h, m, s, f, film()->video_frame_rate()));
663 shared_ptr<dcp::Reel>
664 ReelWriter::create_reel (list<ReferencedReelAsset> const & refs, list<shared_ptr<Font> > const & fonts, boost::filesystem::path output_dcp)
666 LOG_GENERAL ("create_reel for %1-%2; %3 of %4", _period.from.get(), _period.to.get(), _reel_index, _reel_count);
668 shared_ptr<dcp::Reel> reel (new dcp::Reel());
669 shared_ptr<dcp::ReelPictureAsset> reel_picture_asset = create_reel_picture (reel, refs);
670 create_reel_sound (reel, refs);
671 create_reel_text (reel, refs, fonts, reel_picture_asset->actual_duration(), output_dcp);
672 create_reel_markers (reel);
675 reel->add (shared_ptr<dcp::ReelAtmosAsset>(new dcp::ReelAtmosAsset(_atmos_asset, 0)));
682 ReelWriter::calculate_digests (boost::function<void (float)> set_progress)
684 if (_picture_asset) {
685 _picture_asset->hash (set_progress);
689 _sound_asset->hash (set_progress);
693 _atmos_asset->hash (set_progress);
698 ReelWriter::start () const
700 return _period.from.frames_floor (film()->video_frame_rate());
705 ReelWriter::write (shared_ptr<const AudioBuffers> audio)
707 if (!_sound_asset_writer) {
711 DCPOMATIC_ASSERT (audio);
712 _sound_asset_writer->write (audio->data(), audio->frames());
716 ReelWriter::write (PlayerText subs, TextType type, optional<DCPTextTrack> track, DCPTimePeriod period)
718 shared_ptr<dcp::SubtitleAsset> asset;
721 case TEXT_OPEN_SUBTITLE:
722 asset = _subtitle_asset;
724 case TEXT_CLOSED_CAPTION:
725 DCPOMATIC_ASSERT (track);
726 asset = _closed_caption_assets[*track];
729 DCPOMATIC_ASSERT (false);
733 vector<dcp::LanguageTag> lang = film()->subtitle_languages();
734 if (film()->interop()) {
735 shared_ptr<dcp::InteropSubtitleAsset> s (new dcp::InteropSubtitleAsset ());
736 s->set_movie_title (film()->name());
737 if (type == TEXT_OPEN_SUBTITLE) {
738 s->set_language (lang.empty() ? "Unknown" : lang.front().to_string());
739 } else if (!track->language.empty()) {
740 s->set_language (track->language);
742 s->set_reel_number (raw_convert<string> (_reel_index + 1));
745 shared_ptr<dcp::SMPTESubtitleAsset> s (new dcp::SMPTESubtitleAsset ());
746 s->set_content_title_text (film()->name());
747 s->set_metadata (mxf_metadata());
748 if (type == TEXT_OPEN_SUBTITLE && !lang.empty()) {
749 s->set_language (lang.front());
750 } else if (track && !track->language.empty()) {
751 s->set_language (dcp::LanguageTag(track->language));
753 s->set_edit_rate (dcp::Fraction (film()->video_frame_rate(), 1));
754 s->set_reel_number (_reel_index + 1);
755 s->set_time_code_rate (film()->video_frame_rate());
756 s->set_start_time (dcp::Time ());
757 if (film()->encrypted()) {
758 s->set_key (film()->key());
765 case TEXT_OPEN_SUBTITLE:
766 _subtitle_asset = asset;
768 case TEXT_CLOSED_CAPTION:
769 DCPOMATIC_ASSERT (track);
770 _closed_caption_assets[*track] = asset;
773 DCPOMATIC_ASSERT (false);
776 BOOST_FOREACH (StringText i, subs.string) {
777 /* XXX: couldn't / shouldn't we use period here rather than getting time from the subtitle? */
778 i.set_in (i.in() - dcp::Time (_period.from.seconds(), i.in().tcr));
779 i.set_out (i.out() - dcp::Time (_period.from.seconds(), i.out().tcr));
780 asset->add (shared_ptr<dcp::Subtitle>(new dcp::SubtitleString(i)));
783 BOOST_FOREACH (BitmapText i, subs.bitmap) {
785 shared_ptr<dcp::Subtitle>(
786 new dcp::SubtitleImage(
788 dcp::Time(period.from.seconds() - _period.from.seconds(), film()->video_frame_rate()),
789 dcp::Time(period.to.seconds() - _period.from.seconds(), film()->video_frame_rate()),
790 i.rectangle.x, dcp::HALIGN_LEFT, i.rectangle.y, dcp::VALIGN_TOP,
791 dcp::Time(), dcp::Time()
799 ReelWriter::existing_picture_frame_ok (FILE* asset_file, shared_ptr<InfoFileHandle> info_file, Frame frame) const
801 LOG_GENERAL ("Checking existing picture frame %1", frame);
803 /* Read the data from the info file; for 3D we just check the left
804 frames until we find a good one.
806 dcp::FrameInfo const info = read_frame_info (info_file, frame, film()->three_d() ? EYES_LEFT : EYES_BOTH);
810 /* Read the data from the asset and hash it */
811 dcpomatic_fseek (asset_file, info.offset, SEEK_SET);
812 ArrayData data (info.size);
813 size_t const read = fread (data.data(), 1, data.size(), asset_file);
814 LOG_GENERAL ("Read %1 bytes of asset data; wanted %2", read, info.size);
815 if (read != static_cast<size_t> (data.size ())) {
816 LOG_GENERAL ("Existing frame %1 is incomplete", frame);
820 digester.add (data.data(), data.size());
821 LOG_GENERAL ("Hash %1 vs %2", digester.get(), info.hash);
822 if (digester.get() != info.hash) {
823 LOG_GENERAL ("Existing frame %1 failed hash check", frame);