diff options
| author | Carl Hetherington <cth@carlh.net> | 2015-06-04 12:25:48 +0100 |
|---|---|---|
| committer | Carl Hetherington <cth@carlh.net> | 2015-06-05 15:53:48 +0100 |
| commit | 42a65cba0d8da23c12af52015e66cd9dc0b5a5fa (patch) | |
| tree | ff0a017c49d0975f21a5314d2f77be20f68bd379 /src | |
| parent | 943e75e0ac5730714f3823771f127fe78e4cf82b (diff) | |
Initial work on SMPTE subtitles.
Diffstat (limited to 'src')
| -rw-r--r-- | src/dcp_time.cc | 1 | ||||
| -rw-r--r-- | src/interop_subtitle_asset.cc | 127 | ||||
| -rw-r--r-- | src/interop_subtitle_asset.h | 33 | ||||
| -rw-r--r-- | src/mxf.cc | 2 | ||||
| -rw-r--r-- | src/mxf.h | 2 | ||||
| -rw-r--r-- | src/smpte_subtitle_asset.cc | 109 | ||||
| -rw-r--r-- | src/smpte_subtitle_asset.h | 33 | ||||
| -rw-r--r-- | src/subtitle_asset.cc | 147 | ||||
| -rw-r--r-- | src/subtitle_asset.h | 25 | ||||
| -rw-r--r-- | src/types.cc | 7 | ||||
| -rw-r--r-- | src/types.h | 2 |
11 files changed, 326 insertions, 162 deletions
diff --git a/src/dcp_time.cc b/src/dcp_time.cc index d34804e9..5d9bed81 100644 --- a/src/dcp_time.cc +++ b/src/dcp_time.cc @@ -66,6 +66,7 @@ Time::set (double seconds, int tcr_) } } +/** @param time String of the form HH:MM:SS:EE */ Time::Time (string time, int tcr_) : tcr (tcr_) { diff --git a/src/interop_subtitle_asset.cc b/src/interop_subtitle_asset.cc index a6bdfcaf..528b7d90 100644 --- a/src/interop_subtitle_asset.cc +++ b/src/interop_subtitle_asset.cc @@ -22,9 +22,11 @@ #include "xml.h" #include "raw_convert.h" #include "font_node.h" +#include "util.h" #include <libxml++/libxml++.h> #include <boost/foreach.hpp> #include <cmath> +#include <cstdio> using std::list; using std::string; @@ -40,7 +42,8 @@ InteropSubtitleAsset::InteropSubtitleAsset (boost::filesystem::path file) shared_ptr<cxml::Document> xml (new cxml::Document ("DCSubtitle")); xml->read_file (file); _id = xml->string_child ("SubtitleID"); - + _reel_number = xml->string_child ("ReelNumber"); + _language = xml->string_child ("Language"); _movie_title = xml->string_child ("MovieTitle"); _load_font_nodes = type_children<dcp::InteropLoadFontNode> (xml, "LoadFont"); @@ -59,15 +62,6 @@ InteropSubtitleAsset::InteropSubtitleAsset (string movie_title, string language) _language = language; } -struct SubtitleSorter { - bool operator() (SubtitleString const & a, SubtitleString const & b) { - if (a.in() != b.in()) { - return a.in() < b.in(); - } - return a.v_position() < b.v_position(); - } -}; - Glib::ustring InteropSubtitleAsset::xml_as_string () const { @@ -86,102 +80,7 @@ InteropSubtitleAsset::xml_as_string () const load_font->set_attribute ("URI", (*i)->uri); } - list<SubtitleString> sorted = _subtitles; - sorted.sort (SubtitleSorter ()); - - /* XXX: script, underlined, weight not supported */ - - optional<string> font; - bool italic = false; - Colour colour; - int size = 0; - float aspect_adjust = 1.0; - Effect effect = NONE; - Colour effect_colour; - int spot_number = 1; - Time last_in; - Time last_out; - Time last_fade_up_time; - Time last_fade_down_time; - - xmlpp::Element* font_element = 0; - xmlpp::Element* subtitle_element = 0; - - for (list<SubtitleString>::iterator i = sorted.begin(); i != sorted.end(); ++i) { - - /* We will start a new <Font>...</Font> whenever some font property changes. - I suppose we should really make an optimal hierarchy of <Font> tags, but - that seems hard. - */ - - bool const font_changed = - font != i->font() || - italic != i->italic() || - colour != i->colour() || - size != i->size() || - fabs (aspect_adjust - i->aspect_adjust()) > ASPECT_ADJUST_EPSILON || - effect != i->effect() || - effect_colour != i->effect_colour(); - - if (font_changed) { - font = i->font (); - italic = i->italic (); - colour = i->colour (); - size = i->size (); - aspect_adjust = i->aspect_adjust (); - effect = i->effect (); - effect_colour = i->effect_colour (); - } - - if (!font_element || font_changed) { - font_element = root->add_child ("Font"); - if (font) { - font_element->set_attribute ("Id", font.get ()); - } - font_element->set_attribute ("Italic", italic ? "yes" : "no"); - font_element->set_attribute ("Color", colour.to_argb_string()); - font_element->set_attribute ("Size", raw_convert<string> (size)); - if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) { - font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust)); - } - font_element->set_attribute ("Effect", effect_to_string (effect)); - font_element->set_attribute ("EffectColor", effect_colour.to_argb_string()); - font_element->set_attribute ("Script", "normal"); - font_element->set_attribute ("Underlined", "no"); - font_element->set_attribute ("Weight", "normal"); - } - - if (!subtitle_element || font_changed || - (last_in != i->in() || - last_out != i->out() || - last_fade_up_time != i->fade_up_time() || - last_fade_down_time != i->fade_down_time() - )) { - - subtitle_element = font_element->add_child ("Subtitle"); - subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++)); - subtitle_element->set_attribute ("TimeIn", i->in().as_string()); - subtitle_element->set_attribute ("TimeOut", i->out().as_string()); - subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i->fade_up_time().as_editable_units(250))); - subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i->fade_down_time().as_editable_units(250))); - - last_in = i->in (); - last_out = i->out (); - last_fade_up_time = i->fade_up_time (); - last_fade_down_time = i->fade_down_time (); - } - - xmlpp::Element* text = subtitle_element->add_child ("Text"); - if (i->h_align() != HALIGN_CENTER) { - text->set_attribute ("HAlign", halign_to_string (i->h_align ())); - } - if (i->h_position() > ALIGN_EPSILON) { - text->set_attribute ("HPosition", raw_convert<string> (i->h_position() * 100, 6)); - } - text->set_attribute ("VAlign", valign_to_string (i->v_align())); - text->set_attribute ("VPosition", raw_convert<string> (i->v_position() * 100, 6)); - text->add_child_text (i->text()); - } + subtitles_as_xml (root, 250, ""); return doc.write_to_string_formatted ("UTF-8"); } @@ -237,3 +136,19 @@ InteropSubtitleAsset::load_font_nodes () const copy (_load_font_nodes.begin(), _load_font_nodes.end(), back_inserter (lf)); return lf; } + +/** Write this content to an XML file */ +void +InteropSubtitleAsset::write (boost::filesystem::path p) const +{ + FILE* f = fopen_boost (p, "w"); + if (!f) { + throw FileError ("Could not open file for writing", p, -1); + } + + Glib::ustring const s = xml_as_string (); + fwrite (s.c_str(), 1, s.bytes(), f); + fclose (f); + + _file = p; +} diff --git a/src/interop_subtitle_asset.h b/src/interop_subtitle_asset.h index 586bb67c..2a1fae7f 100644 --- a/src/interop_subtitle_asset.h +++ b/src/interop_subtitle_asset.h @@ -41,8 +41,41 @@ public: void add_font (std::string id, std::string uri); Glib::ustring xml_as_string () const; + void write (boost::filesystem::path path) const; + + void set_reel_number (std::string n) { + _reel_number = n; + } + + void set_language (std::string l) { + _language = l; + } + + void set_movie_title (std::string m) { + _movie_title = m; + } + + std::string reel_number () const { + return _reel_number; + } + + std::string language () const { + return _language; + } + + std::string movie_title () const { + return _movie_title; + } + +protected: + + std::string pkl_type (Standard) const { + return "text/xml"; + } private: + std::string _reel_number; + std::string _language; std::string _movie_title; std::list<boost::shared_ptr<InteropLoadFontNode> > _load_font_nodes; }; @@ -54,7 +54,7 @@ MXF::~MXF () } void -MXF::fill_writer_info (ASDCP::WriterInfo* writer_info, string id, Standard standard) +MXF::fill_writer_info (ASDCP::WriterInfo* writer_info, string id, Standard standard) const { writer_info->ProductVersion = _metadata.product_version; writer_info->CompanyName = _metadata.company_name; @@ -95,7 +95,7 @@ protected: * @param w struct to fill in. * @param standard INTEROP or SMPTE. */ - void fill_writer_info (ASDCP::WriterInfo* w, std::string id, Standard standard); + void fill_writer_info (ASDCP::WriterInfo* w, std::string id, Standard standard) const; ASDCP::AESDecContext* _decryption_context; /** ID of the key used for encryption/decryption, if there is one */ diff --git a/src/smpte_subtitle_asset.cc b/src/smpte_subtitle_asset.cc index e5a083b1..40afc76d 100644 --- a/src/smpte_subtitle_asset.cc +++ b/src/smpte_subtitle_asset.cc @@ -24,13 +24,19 @@ #include "xml.h" #include "AS_DCP.h" #include "KM_util.h" +#include "raw_convert.h" +#include <libxml++/libxml++.h> #include <boost/foreach.hpp> +#include <boost/algorithm/string.hpp> using std::string; using std::list; using std::stringstream; using std::cout; +using std::vector; using boost::shared_ptr; +using boost::split; +using boost::is_any_of; using namespace dcp; SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file, bool mxf) @@ -64,16 +70,37 @@ SMPTESubtitleAsset::SMPTESubtitleAsset (boost::filesystem::path file, bool mxf) _load_font_nodes = type_children<dcp::SMPTELoadFontNode> (xml, "LoadFont"); - int tcr = xml->number_child<int> ("TimeCodeRate"); + _content_title_text = xml->string_child ("ContentTitleText"); + _annotation_text = xml->optional_string_child ("AnnotationText"); + _issue_date = LocalTime (xml->string_child ("IssueDate")); + _reel_number = xml->optional_number_child<int> ("ReelNumber"); + _language = xml->optional_string_child ("Language"); + + /* This is supposed to be two numbers, but a single number has been seen in the wild */ + string const er = xml->string_child ("EditRate"); + vector<string> er_parts; + split (er_parts, er, is_any_of (" ")); + if (er_parts.size() == 1) { + _edit_rate = Fraction (raw_convert<int> (er_parts[0]), 1); + } else if (er_parts.size() == 2) { + _edit_rate = Fraction (raw_convert<int> (er_parts[0]), raw_convert<int> (er_parts[1])); + } else { + throw XMLError ("malformed EditRate " + er); + } + + _time_code_rate = xml->number_child<int> ("TimeCodeRate"); + if (xml->optional_string_child ("StartTime")) { + _start_time = Time (xml->string_child ("StartTime"), _time_code_rate); + } shared_ptr<cxml::Node> subtitle_list = xml->optional_node_child ("SubtitleList"); list<cxml::NodePtr> f = subtitle_list->node_children ("Font"); list<shared_ptr<dcp::FontNode> > font_nodes; BOOST_FOREACH (cxml::NodePtr& i, f) { - font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, tcr))); + font_nodes.push_back (shared_ptr<FontNode> (new FontNode (i, _time_code_rate))); } - + parse_common (xml, font_nodes); } @@ -92,3 +119,79 @@ SMPTESubtitleAsset::valid_mxf (boost::filesystem::path file) Kumu::Result_t r = reader.OpenRead (file.string().c_str ()); return !ASDCP_FAILURE (r); } + +Glib::ustring +SMPTESubtitleAsset::xml_as_string () const +{ + xmlpp::Document doc; + xmlpp::Element* root = doc.create_root_node ("dcst:SubtitleReel"); + root->set_namespace_declaration ("http://www.smpte-ra.org/schemas/428-7/2010/DCST", "dcst"); + root->set_namespace_declaration ("http://www.w3.org/2001/XMLSchema", "xs"); + + root->add_child("ID", "dcst")->add_child_text (_id); + root->add_child("ContentTitleText", "dcst")->add_child_text (_content_title_text); + if (_annotation_text) { + root->add_child("AnnotationText", "dcst")->add_child_text (_annotation_text.get ()); + } + root->add_child("IssueDate", "dcst")->add_child_text (_issue_date.as_string (true)); + if (_reel_number) { + root->add_child("ReelNumber", "dcst")->add_child_text (raw_convert<string> (_reel_number.get ())); + } + if (_language) { + root->add_child("Language", "dcst")->add_child_text (_language.get ()); + } + root->add_child("EditRate", "dcst")->add_child_text (_edit_rate.as_string ()); + root->add_child("TimeCodeRate", "dcst")->add_child_text (raw_convert<string> (_time_code_rate)); + if (_start_time) { + root->add_child("StartTime", "dcst")->add_child_text (_start_time.get().as_string ()); + } + + BOOST_FOREACH (shared_ptr<SMPTELoadFontNode> i, _load_font_nodes) { + xmlpp::Element* load_font = root->add_child("LoadFont", "dcst"); + load_font->add_child_text (i->urn); + load_font->set_attribute ("ID", i->id); + } + + subtitles_as_xml (root->add_child ("SubtitleList", "dcst"), _time_code_rate, "dcst"); + + return doc.write_to_string_formatted ("UTF-8"); +} + +/** Write this content to a MXF file */ +void +SMPTESubtitleAsset::write (boost::filesystem::path p) const +{ + ASDCP::WriterInfo writer_info; + fill_writer_info (&writer_info, _id, SMPTE); + + ASDCP::TimedText::TimedTextDescriptor descriptor; + descriptor.EditRate = ASDCP::Rational (_edit_rate.numerator, _edit_rate.denominator); + descriptor.EncodingName = "UTF-8"; + descriptor.ResourceList.clear (); + descriptor.NamespaceName = "dcst"; + memcpy (descriptor.AssetID, writer_info.AssetUUID, ASDCP::UUIDlen); + descriptor.ContainerDuration = latest_subtitle_out().as_editable_units (_edit_rate.numerator / _edit_rate.denominator); + + /* XXX: fonts into descriptor? */ + + ASDCP::TimedText::MXFWriter writer; + Kumu::Result_t r = writer.OpenWrite (p.string().c_str(), writer_info, descriptor); + if (ASDCP_FAILURE (r)) { + boost::throw_exception (FileError ("could not open subtitle MXF for writing", p.string(), r)); + } + + /* XXX: no encryption */ + writer.WriteTimedTextResource (xml_as_string ()); + + writer.Finalize (); + + _file = p; +} + +bool +SMPTESubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions options, NoteHandler note) const +{ + /* XXX */ + return false; +} + diff --git a/src/smpte_subtitle_asset.h b/src/smpte_subtitle_asset.h index 639c8eb7..5f4d8833 100644 --- a/src/smpte_subtitle_asset.h +++ b/src/smpte_subtitle_asset.h @@ -18,13 +18,15 @@ */ #include "subtitle_asset.h" +#include "local_time.h" +#include "mxf.h" #include <boost/filesystem.hpp> namespace dcp { class SMPTELoadFontNode; -class SMPTESubtitleAsset : public SubtitleAsset +class SMPTESubtitleAsset : public SubtitleAsset, public MXF { public: /** @param file File name @@ -32,11 +34,40 @@ public: */ SMPTESubtitleAsset (boost::filesystem::path file, bool mxf = true); + bool equals ( + boost::shared_ptr<const Asset>, + EqualityOptions, + NoteHandler note + ) const; + std::list<boost::shared_ptr<LoadFontNode> > load_font_nodes () const; + Glib::ustring xml_as_string () const; + void write (boost::filesystem::path path) const; + + /** @return language, if one was specified */ + boost::optional<std::string> language () const { + return _language; + } + static bool valid_mxf (boost::filesystem::path); + +protected: + + std::string pkl_type (Standard) const { + return "application/mxf"; + } private: + std::string _content_title_text; + boost::optional<std::string> _annotation_text; + LocalTime _issue_date; + boost::optional<int> _reel_number; + boost::optional<std::string> _language; + Fraction _edit_rate; + int _time_code_rate; + boost::optional<Time> _start_time; + std::list<boost::shared_ptr<SMPTELoadFontNode> > _load_font_nodes; }; diff --git a/src/subtitle_asset.cc b/src/subtitle_asset.cc index f1ecc939..1c341de4 100644 --- a/src/subtitle_asset.cc +++ b/src/subtitle_asset.cc @@ -43,14 +43,12 @@ using boost::dynamic_pointer_cast; using namespace dcp; SubtitleAsset::SubtitleAsset () - : _reel_number ("1") { } SubtitleAsset::SubtitleAsset (boost::filesystem::path file) : Asset (file) - , _reel_number ("1") { } @@ -58,13 +56,7 @@ SubtitleAsset::SubtitleAsset (boost::filesystem::path file) void SubtitleAsset::parse_common (shared_ptr<cxml::Document> xml, list<shared_ptr<dcp::FontNode> > font_nodes) { - _reel_number = xml->string_child ("ReelNumber"); - _language = xml->string_child ("Language"); - - /* Now make Subtitle objects to represent the raw XML nodes - in a sane way. - */ - + /* Make Subtitle objects to represent the raw XML nodes in a sane way */ ParseState parse_state; examine_font_nodes (xml, font_nodes, parse_state); } @@ -169,21 +161,6 @@ SubtitleAsset::add (SubtitleString s) _subtitles.push_back (s); } -void -SubtitleAsset::write_xml (boost::filesystem::path p) const -{ - FILE* f = fopen_boost (p, "w"); - if (!f) { - throw FileError ("Could not open file for writing", p, -1); - } - - Glib::ustring const s = xml_as_string (); - fwrite (s.c_str(), 1, s.bytes(), f); - fclose (f); - - _file = p; -} - Time SubtitleAsset::latest_subtitle_out () const { @@ -209,16 +186,6 @@ SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions opti return false; } - if (_reel_number != other->_reel_number) { - note (DCP_ERROR, "subtitle reel numbers differ"); - return false; - } - - if (_language != other->_language) { - note (DCP_ERROR, "subtitle languages differ"); - return false; - } - if (_subtitles != other->_subtitles) { note (DCP_ERROR, "subtitles differ"); return false; @@ -226,3 +193,115 @@ SubtitleAsset::equals (shared_ptr<const Asset> other_asset, EqualityOptions opti return true; } + +struct SubtitleSorter { + bool operator() (SubtitleString const & a, SubtitleString const & b) { + if (a.in() != b.in()) { + return a.in() < b.in(); + } + return a.v_position() < b.v_position(); + } +}; + +void +SubtitleAsset::subtitles_as_xml (xmlpp::Element* root, int time_code_rate, string xmlns) const +{ + list<SubtitleString> sorted = _subtitles; + sorted.sort (SubtitleSorter ()); + + /* XXX: script, underlined, weight not supported */ + + optional<string> font; + bool italic = false; + Colour colour; + int size = 0; + float aspect_adjust = 1.0; + Effect effect = NONE; + Colour effect_colour; + int spot_number = 1; + Time last_in; + Time last_out; + Time last_fade_up_time; + Time last_fade_down_time; + + xmlpp::Element* font_element = 0; + xmlpp::Element* subtitle_element = 0; + + for (list<SubtitleString>::iterator i = sorted.begin(); i != sorted.end(); ++i) { + + /* We will start a new <Font>...</Font> whenever some font property changes. + I suppose we should really make an optimal hierarchy of <Font> tags, but + that seems hard. + */ + + bool const font_changed = + font != i->font() || + italic != i->italic() || + colour != i->colour() || + size != i->size() || + fabs (aspect_adjust - i->aspect_adjust()) > ASPECT_ADJUST_EPSILON || + effect != i->effect() || + effect_colour != i->effect_colour(); + + if (font_changed) { + font = i->font (); + italic = i->italic (); + colour = i->colour (); + size = i->size (); + aspect_adjust = i->aspect_adjust (); + effect = i->effect (); + effect_colour = i->effect_colour (); + } + + if (!font_element || font_changed) { + font_element = root->add_child ("Font", xmlns); + if (font) { + font_element->set_attribute ("Id", font.get ()); + } + font_element->set_attribute ("Italic", italic ? "yes" : "no"); + font_element->set_attribute ("Color", colour.to_argb_string()); + font_element->set_attribute ("Size", raw_convert<string> (size)); + if (fabs (aspect_adjust - 1.0) > ASPECT_ADJUST_EPSILON) { + font_element->set_attribute ("AspectAdjust", raw_convert<string> (aspect_adjust)); + } + font_element->set_attribute ("Effect", effect_to_string (effect)); + font_element->set_attribute ("EffectColor", effect_colour.to_argb_string()); + font_element->set_attribute ("Script", "normal"); + font_element->set_attribute ("Underlined", "no"); + font_element->set_attribute ("Weight", "normal"); + } + + if (!subtitle_element || font_changed || + (last_in != i->in() || + last_out != i->out() || + last_fade_up_time != i->fade_up_time() || + last_fade_down_time != i->fade_down_time() + )) { + + subtitle_element = font_element->add_child ("Subtitle", xmlns); + subtitle_element->set_attribute ("SpotNumber", raw_convert<string> (spot_number++)); + subtitle_element->set_attribute ("TimeIn", i->in().rebase(time_code_rate).as_string()); + subtitle_element->set_attribute ("TimeOut", i->out().rebase(time_code_rate).as_string()); + subtitle_element->set_attribute ("FadeUpTime", raw_convert<string> (i->fade_up_time().as_editable_units(time_code_rate))); + subtitle_element->set_attribute ("FadeDownTime", raw_convert<string> (i->fade_down_time().as_editable_units(time_code_rate))); + + last_in = i->in (); + last_out = i->out (); + last_fade_up_time = i->fade_up_time (); + last_fade_down_time = i->fade_down_time (); + } + + xmlpp::Element* text = subtitle_element->add_child ("Text", xmlns); + if (i->h_align() != HALIGN_CENTER) { + text->set_attribute ("HAlign", halign_to_string (i->h_align ())); + } + if (i->h_position() > ALIGN_EPSILON) { + text->set_attribute ("HPosition", raw_convert<string> (i->h_position() * 100, 6)); + } + text->set_attribute ("VAlign", valign_to_string (i->v_align())); + text->set_attribute ("VPosition", raw_convert<string> (i->v_position() * 100, 6)); + text->add_child_text (i->text()); + } +} + + diff --git a/src/subtitle_asset.h b/src/subtitle_asset.h index deb72ece..c8100d58 100644 --- a/src/subtitle_asset.h +++ b/src/subtitle_asset.h @@ -25,6 +25,10 @@ #include "subtitle_string.h" #include <libcxml/cxml.h> +namespace xmlpp { + class Element; +} + namespace dcp { @@ -49,10 +53,6 @@ public: NoteHandler note ) const; - std::string language () const { - return _language; - } - std::list<SubtitleString> subtitles_during (Time from, Time to) const; std::list<SubtitleString> const & subtitles () const { return _subtitles; @@ -60,11 +60,8 @@ public: void add (SubtitleString); - void write_xml (boost::filesystem::path) const; - virtual Glib::ustring xml_as_string () const { - /* XXX: this should be pure virtual when SMPTE writing is implemented */ - return ""; - } + virtual void write (boost::filesystem::path) const = 0; + virtual Glib::ustring xml_as_string () const = 0; Time latest_subtitle_out () const; @@ -73,18 +70,14 @@ public: protected: void parse_common (boost::shared_ptr<cxml::Document> xml, std::list<boost::shared_ptr<FontNode> > font_nodes); - std::string pkl_type (Standard) const { - return "text/xml"; - } + virtual std::string pkl_type (Standard) const = 0; std::string asdcp_kind () const { return "Subtitle"; } - - /* strangely, this is sometimes a string */ - std::string _reel_number; - std::string _language; + void subtitles_as_xml (xmlpp::Element* root, int time_code_rate, std::string xmlns) const; + std::list<SubtitleString> _subtitles; private: diff --git a/src/types.cc b/src/types.cc index e86e999c..37666701 100644 --- a/src/types.cc +++ b/src/types.cc @@ -20,6 +20,7 @@ #include "raw_convert.h" #include "types.h" #include "exceptions.h" +#include "compose.hpp" #include <boost/algorithm/string.hpp> #include <vector> #include <cstdio> @@ -43,6 +44,12 @@ Fraction::Fraction (string s) denominator = raw_convert<int> (b[1]); } +string +Fraction::as_string () const +{ + return String::compose ("%1 %2", numerator, denominator); +} + bool dcp::operator== (Fraction const & a, Fraction const & b) { diff --git a/src/types.h b/src/types.h index 626666b4..ca7603ab 100644 --- a/src/types.h +++ b/src/types.h @@ -139,6 +139,8 @@ public: return float (numerator) / denominator; } + std::string as_string () const; + int numerator; int denominator; }; |
