diff options
| author | Carl Hetherington <cth@carlh.net> | 2024-12-06 17:46:24 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2024-12-26 17:20:25 +0100 |
| commit | 54e2644ed4dcc4faf5135e60926cfdda14886fea (patch) | |
| tree | ca1599fe32e3ace691dffcdb6ded45ad29baea59 /src | |
| parent | 1f077d2b791a059ccef4069dca07ead990bd568e (diff) | |
Support FCP XML files containing subtitles (#2909).
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib/content_factory.cc | 5 | ||||
| -rw-r--r-- | src/lib/decoder_factory.cc | 6 | ||||
| -rw-r--r-- | src/lib/exceptions.h | 9 | ||||
| -rw-r--r-- | src/lib/fcpxml.cc | 82 | ||||
| -rw-r--r-- | src/lib/fcpxml.h | 55 | ||||
| -rw-r--r-- | src/lib/fcpxml_content.cc | 108 | ||||
| -rw-r--r-- | src/lib/fcpxml_content.h | 57 | ||||
| -rw-r--r-- | src/lib/fcpxml_decoder.cc | 105 | ||||
| -rw-r--r-- | src/lib/fcpxml_decoder.h | 50 | ||||
| -rw-r--r-- | src/lib/wscript | 3 | ||||
| -rw-r--r-- | src/wx/text_panel.cc | 4 |
11 files changed, 483 insertions, 1 deletions
diff --git a/src/lib/content_factory.cc b/src/lib/content_factory.cc index 9f464896a..fa3f0876c 100644 --- a/src/lib/content_factory.cc +++ b/src/lib/content_factory.cc @@ -29,6 +29,7 @@ #include "dcp_content.h" #include "dcp_subtitle_content.h" #include "dcpomatic_log.h" +#include "fcpxml_content.h" #include "ffmpeg_audio_stream.h" #include "ffmpeg_content.h" #include "film.h" @@ -97,6 +98,8 @@ content_factory(cxml::ConstNodePtr node, boost::optional<boost::filesystem::path content = std::make_shared<VideoMXFContent>(node, film_directory, version); } else if (type == "AtmosMXF") { content = std::make_shared<AtmosMXFContent>(node, film_directory, version); + } else if (type == "FCPXML") { + content = std::make_shared<FCPXMLContent>(node, film_directory, version, notes); } return content; @@ -178,6 +181,8 @@ content_factory (boost::filesystem::path path) throw KDMAsContentError (); } single = std::make_shared<DCPSubtitleContent>(path); + } else if (ext == ".fcpxml") { + single = std::make_shared<FCPXMLContent>(path); } else if (ext == ".mxf" && dcp::SMPTETextAsset::valid_mxf(path)) { single = std::make_shared<DCPSubtitleContent>(path); } else if (ext == ".mxf" && VideoMXFContent::valid_mxf(path)) { diff --git a/src/lib/decoder_factory.cc b/src/lib/decoder_factory.cc index ea0eda83d..31156ab3f 100644 --- a/src/lib/decoder_factory.cc +++ b/src/lib/decoder_factory.cc @@ -25,6 +25,8 @@ #include "dcp_decoder.h" #include "dcp_subtitle_content.h" #include "dcp_subtitle_decoder.h" +#include "fcpxml_content.h" +#include "fcpxml_decoder.h" #include "ffmpeg_content.h" #include "ffmpeg_decoder.h" #include "image_content.h" @@ -92,5 +94,9 @@ decoder_factory (shared_ptr<const Film> film, shared_ptr<const Content> content, return make_shared<AtmosMXFDecoder>(film, amc); } + if (auto c = dynamic_pointer_cast<const FCPXMLContent>(content)) { + return make_shared<FCPXMLDecoder>(film, c); + } + return {}; } diff --git a/src/lib/exceptions.h b/src/lib/exceptions.h index 3069d792e..c1bb8f6b6 100644 --- a/src/lib/exceptions.h +++ b/src/lib/exceptions.h @@ -542,4 +542,13 @@ public: }; +class FCPXMLError : public std::runtime_error +{ +public: + explicit FCPXMLError(std::string s) + : std::runtime_error(s) + {} +}; + + #endif diff --git a/src/lib/fcpxml.cc b/src/lib/fcpxml.cc new file mode 100644 index 000000000..49798381c --- /dev/null +++ b/src/lib/fcpxml.cc @@ -0,0 +1,82 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "fcpxml.h" +#include <dcp/raw_convert.h> +#include <libcxml/cxml.h> +#include <boost/algorithm/string.hpp> +#include <boost/filesystem.hpp> + + +#include "i18n.h" + + +using std::map; +using std::string; +using std::vector; + + +static dcpomatic::ContentTime +convert_time(string const& time) +{ + vector<string> parts; + boost::algorithm::split(parts, time, boost::is_any_of("/")); + + if (parts.size() != 2 || parts[1].empty() || parts[1][parts[1].length() - 1] != 's') { + throw FCPXMLError(String::compose("Unexpected time format %1", time)); + } + + return dcpomatic::ContentTime{dcp::raw_convert<int64_t>(parts[0]) * dcpomatic::ContentTime::HZ / dcp::raw_convert<int64_t>(parts[1])}; +} + + +dcpomatic::fcpxml::Sequence +dcpomatic::fcpxml::load(boost::filesystem::path xml_file) +{ + cxml::Document doc("fcpxml"); + doc.read_file(xml_file); + + auto project = doc.node_child("project"); + + map<string, boost::filesystem::path> assets; + for (auto asset: project->node_child("resources")->node_children("asset")) { + assets[asset->string_attribute("name")] = asset->string_attribute("src"); + } + + auto sequence = Sequence{xml_file.parent_path()}; + for (auto video: project->node_child("sequence")->node_child("spine")->node_children("video")) { + auto name = video->string_attribute("name"); + auto iter = assets.find(name); + if (iter == assets.end()) { + throw FCPXMLError(String::compose(_("Video refers to missing asset %1"), name)); + } + + auto start = convert_time(video->string_attribute("offset")); + sequence.video.push_back( + { + iter->second, + dcpomatic::ContentTimePeriod(start, start + convert_time(video->string_attribute("duration"))) + } + ); + } + + return sequence; +} diff --git a/src/lib/fcpxml.h b/src/lib/fcpxml.h new file mode 100644 index 000000000..00137ca32 --- /dev/null +++ b/src/lib/fcpxml.h @@ -0,0 +1,55 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "dcpomatic_time.h" +#include <vector> + + +namespace dcpomatic { +namespace fcpxml { + + +class Video +{ +public: + boost::filesystem::path source; ///< filename of PNG relative to Sequence::parent + dcpomatic::ContentTimePeriod period; +}; + + + +class Sequence +{ +public: + Sequence(boost::filesystem::path parent_) + : parent(parent_) + {} + + boost::filesystem::path parent; ///< directory containing the PNG files + std::vector<Video> video; +}; + + +Sequence load(boost::filesystem::path xml_file); + + +} +} diff --git a/src/lib/fcpxml_content.cc b/src/lib/fcpxml_content.cc new file mode 100644 index 000000000..94d1197df --- /dev/null +++ b/src/lib/fcpxml_content.cc @@ -0,0 +1,108 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "fcpxml.h" +#include "fcpxml_content.h" +#include "text_content.h" +#include <dcp/raw_convert.h> + +#include "i18n.h" + + +using std::list; +using std::make_shared; +using std::shared_ptr; +using std::string; +using boost::optional; + + +FCPXMLContent::FCPXMLContent(boost::filesystem::path path) + : Content(path) +{ + text.push_back(make_shared<TextContent>(this, TextType::OPEN_SUBTITLE, TextType::OPEN_SUBTITLE)); +} + + +FCPXMLContent::FCPXMLContent(cxml::ConstNodePtr node, optional<boost::filesystem::path> film_directory, int version, list<string>& notes) + : Content(node, film_directory) +{ + text = TextContent::from_xml(this, node, version, notes); +} + + + +void +FCPXMLContent::examine(shared_ptr<const Film> film, shared_ptr<Job> job) +{ + Content::examine(film, job); + + auto sequence = dcpomatic::fcpxml::load(path(0)); + + boost::mutex::scoped_lock lm(_mutex); + only_text()->set_use(true); + if (!sequence.video.empty()) { + _length = sequence.video.back().period.to; + } +} + + +dcpomatic::DCPTime +FCPXMLContent::full_length(shared_ptr<const Film> film) const +{ + FrameRateChange const frc(film, shared_from_this()); + return { _length, frc }; +} + + +dcpomatic::DCPTime +FCPXMLContent::approximate_length() const +{ + return { _length, {} }; +} + + +string +FCPXMLContent::summary() const +{ + return path_summary() + " " + _("[subtitles]"); +} + + +string +FCPXMLContent::technical_summary() const +{ + return Content::technical_summary() + " - " + _("FCP XML subtitles"); +} + + +void +FCPXMLContent::as_xml(xmlpp::Element* element, bool with_paths, PathBehaviour path_behaviour, optional<boost::filesystem::path> film_directory) const +{ + cxml::add_child(element, "Type", "FCPXML"); + Content::as_xml(element, with_paths, path_behaviour, film_directory); + + if (only_text()) { + only_text()->as_xml(element); + } + + cxml::add_child(element, "Length", dcp::raw_convert<string>(_length.get())); +} + diff --git a/src/lib/fcpxml_content.h b/src/lib/fcpxml_content.h new file mode 100644 index 000000000..701fd6d02 --- /dev/null +++ b/src/lib/fcpxml_content.h @@ -0,0 +1,57 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_FCPXML_CONTENT_H +#define DCPOMATIC_FCPXML_CONTENT_H + + +#include "content.h" + + + +class FCPXMLContent : public Content +{ +public: + FCPXMLContent(boost::filesystem::path path); + FCPXMLContent(cxml::ConstNodePtr node, boost::optional<boost::filesystem::path> film_directory, int version, std::list<std::string>& notes); + + std::shared_ptr<FCPXMLContent> shared_from_this() { + return std::dynamic_pointer_cast<FCPXMLContent>(Content::shared_from_this()); + } + + std::shared_ptr<const FCPXMLContent> shared_from_this() const { + return std::dynamic_pointer_cast<const FCPXMLContent>(Content::shared_from_this()); + } + + void examine(std::shared_ptr<const Film> film, std::shared_ptr<Job>) override; + std::string summary() const override; + std::string technical_summary() const override; + void as_xml(xmlpp::Element*, bool with_paths, PathBehaviour path_behaviour, boost::optional<boost::filesystem::path> film_directory) const override; + dcpomatic::DCPTime full_length(std::shared_ptr<const Film> film) const override; + dcpomatic::DCPTime approximate_length() const override; + +private: + dcpomatic::ContentTime _length; +}; + + + +#endif diff --git a/src/lib/fcpxml_decoder.cc b/src/lib/fcpxml_decoder.cc new file mode 100644 index 000000000..edb19e639 --- /dev/null +++ b/src/lib/fcpxml_decoder.cc @@ -0,0 +1,105 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#include "fcpxml_content.h" +#include "fcpxml_decoder.h" +#include "ffmpeg_image_proxy.h" +#include "guess_crop.h" +#include "image.h" +#include "rect.h" +#include "text_decoder.h" +#include <dcp/array_data.h> + + + +using std::make_shared; +using std::shared_ptr; +using std::weak_ptr; + + +FCPXMLDecoder::FCPXMLDecoder(weak_ptr<const Film> film, shared_ptr<const FCPXMLContent> content) + : Decoder(film) + , _fcpxml_content(content) + , _sequence(dcpomatic::fcpxml::load(content->path(0))) +{ + text.push_back(make_shared<TextDecoder>(this, content->only_text())); + update_position(); +} + + +bool +FCPXMLDecoder::pass() +{ + if (_next >= static_cast<int>(_sequence.video.size())) { + return true; + } + + auto const png_data = dcp::ArrayData(_sequence.parent / _sequence.video[_next].source); + auto const full_image = FFmpegImageProxy(png_data).image(Image::Alignment::PADDED).image; + auto const crop = guess_crop_by_alpha(full_image); + auto const cropped_image = full_image->crop(crop); + + auto rectangle = dcpomatic::Rect<double>{ + static_cast<double>(crop.left) / full_image->size().width, + static_cast<double>(crop.top) / full_image->size().height, + static_cast<double>(cropped_image->size().width) / full_image->size().width, + static_cast<double>(cropped_image->size().height) / full_image->size().height + }; + + only_text()->emit_bitmap(_sequence.video[_next].period, cropped_image, rectangle); + + ++_next; + + update_position(); + return false; +} + + +void +FCPXMLDecoder::seek(dcpomatic::ContentTime time, bool accurate) +{ + /* It's worth back-tracking a little here as decoding is cheap and it's nice if we don't miss + too many subtitles when seeking. + */ + time -= dcpomatic::ContentTime::from_seconds(5); + if (time < dcpomatic::ContentTime()) { + time = {}; + } + + Decoder::seek(time, accurate); + + _next = 0; + while (_next < static_cast<int>(_sequence.video.size()) && _sequence.video[_next].period.from < time) { + ++_next; + } + + update_position(); +} + + +void +FCPXMLDecoder::update_position() +{ + if (_next < static_cast<int>(_sequence.video.size())) { + only_text()->maybe_set_position(_sequence.video[_next].period.from); + } +} + diff --git a/src/lib/fcpxml_decoder.h b/src/lib/fcpxml_decoder.h new file mode 100644 index 000000000..782693256 --- /dev/null +++ b/src/lib/fcpxml_decoder.h @@ -0,0 +1,50 @@ +/* + Copyright (C) 2024 Carl Hetherington <cth@carlh.net> + + This file is part of DCP-o-matic. + + DCP-o-matic is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + DCP-o-matic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with DCP-o-matic. If not, see <http://www.gnu.org/licenses/>. + +*/ + + +#ifndef DCPOMATIC_FCPXML_DECODER_H +#define DCPOMATIC_FCPXML_DECODER_H + + +#include "decoder.h" +#include "fcpxml.h" + + +class FCPXMLContent; + + +class FCPXMLDecoder : public Decoder +{ +public: + FCPXMLDecoder(std::weak_ptr<const Film> film, std::shared_ptr<const FCPXMLContent> content); + + bool pass() override; + void seek(dcpomatic::ContentTime time, bool accurate) override; + +private: + void update_position(); + + std::shared_ptr<const FCPXMLContent> _fcpxml_content; + dcpomatic::fcpxml::Sequence _sequence; + int _next = 0; +}; + + +#endif diff --git a/src/lib/wscript b/src/lib/wscript index dfe3ce487..8a4b17dbc 100644 --- a/src/lib/wscript +++ b/src/lib/wscript @@ -99,6 +99,9 @@ sources = """ examine_ffmpeg_subtitles_job.cc exceptions.cc export_config.cc + fcpxml.cc + fcpxml_content.cc + fcpxml_decoder.cc frame_info.cc file_group.cc file_log.cc diff --git a/src/wx/text_panel.cc b/src/wx/text_panel.cc index 382173a18..0625b4187 100644 --- a/src/wx/text_panel.cc +++ b/src/wx/text_panel.cc @@ -39,6 +39,7 @@ #include "lib/dcp_subtitle_content.h" #include "lib/dcp_subtitle_decoder.h" #include "lib/decoder_factory.h" +#include "lib/fcpxml_content.h" #include "lib/ffmpeg_content.h" #include "lib/ffmpeg_subtitle_stream.h" #include "lib/film.h" @@ -533,6 +534,7 @@ TextPanel::setup_sensitivity () auto sc = std::dynamic_pointer_cast<const StringTextFileContent>(i); auto dc = std::dynamic_pointer_cast<const DCPContent>(i); auto dsc = std::dynamic_pointer_cast<const DCPSubtitleContent>(i); + auto fcp = std::dynamic_pointer_cast<const FCPXMLContent>(i); if (fc) { if (!fc->text.empty()) { ++ffmpeg_subs; @@ -541,7 +543,7 @@ TextPanel::setup_sensitivity () } else if (dc || dsc) { ++dcp_subs; ++any_subs; - } else if (sc) { + } else if (sc || fcp) { /* XXX: in the future there could be bitmap subs from DCPs */ ++any_subs; } |
