Support FCP XML files containing subtitles (#2909).
authorCarl Hetherington <cth@carlh.net>
Fri, 6 Dec 2024 16:46:24 +0000 (17:46 +0100)
committerCarl Hetherington <cth@carlh.net>
Thu, 26 Dec 2024 16:20:25 +0000 (17:20 +0100)
src/lib/content_factory.cc
src/lib/decoder_factory.cc
src/lib/exceptions.h
src/lib/fcpxml.cc [new file with mode: 0644]
src/lib/fcpxml.h [new file with mode: 0644]
src/lib/fcpxml_content.cc [new file with mode: 0644]
src/lib/fcpxml_content.h [new file with mode: 0644]
src/lib/fcpxml_decoder.cc [new file with mode: 0644]
src/lib/fcpxml_decoder.h [new file with mode: 0644]
src/lib/wscript
src/wx/text_panel.cc

index 9f464896abada4bd56c85a991aeab540fc659bb2..fa3f0876c120e5544b23fcf36800cfca5f1bb2c0 100644 (file)
@@ -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)) {
index ea0eda83d1bcdf3af6bcb1a7577eae43ea1a14f8..31156ab3ffac7098ad00b1b271eb07800dee0292 100644 (file)
@@ -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 {};
 }
index 3069d792e77aaa2f61b649c3778873020bf50157..c1bb8f6b6c3680713872a47d3db29c12e779cdca 100644 (file)
@@ -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 (file)
index 0000000..4979838
--- /dev/null
@@ -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 (file)
index 0000000..00137ca
--- /dev/null
@@ -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 (file)
index 0000000..94d1197
--- /dev/null
@@ -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 (file)
index 0000000..701fd6d
--- /dev/null
@@ -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 (file)
index 0000000..edb19e6
--- /dev/null
@@ -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 (file)
index 0000000..7826932
--- /dev/null
@@ -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
index dfe3ce4872cd48f8dab08a05cce5b3befa0675d4..8a4b17dbc98039d0d3b6bcfffaee308fc08f8647 100644 (file)
@@ -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
index 382173a18170c0253e49e7633f3bda69daeb2deb..0625b4187c820b6ecf8b05446ae382e3fc5df40b 100644 (file)
@@ -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;
                }