Merge 1.0-seek and subtitle-content.
authorCarl Hetherington <cth@carlh.net>
Wed, 15 Jan 2014 21:23:33 +0000 (21:23 +0000)
committerCarl Hetherington <cth@carlh.net>
Wed, 15 Jan 2014 21:23:33 +0000 (21:23 +0000)
27 files changed:
src/lib/content_factory.cc
src/lib/decoded.h
src/lib/exceptions.cc
src/lib/exceptions.h
src/lib/ffmpeg_decoder.cc
src/lib/player.cc
src/lib/player.h
src/lib/render_subtitles.cc [new file with mode: 0644]
src/lib/render_subtitles.h [new file with mode: 0644]
src/lib/subrip.cc [new file with mode: 0644]
src/lib/subrip.h [new file with mode: 0644]
src/lib/subrip_content.cc [new file with mode: 0644]
src/lib/subrip_content.h [new file with mode: 0644]
src/lib/subrip_decoder.cc [new file with mode: 0644]
src/lib/subrip_decoder.h [new file with mode: 0644]
src/lib/subrip_subtitle.h [new file with mode: 0644]
src/lib/subtitle_decoder.cc
src/lib/subtitle_decoder.h
src/lib/util.cc
src/lib/wscript
src/wx/film_editor.cc
test/ratio_test.cc
test/subrip_test.cc [new file with mode: 0644]
test/test.cc
test/test.h
test/wscript
wscript

index bab22b8eb19e14b6179fd27696710c6b630df70c..825c8049854ba3032519b7d27430560a18d9f8b8 100644 (file)
@@ -21,6 +21,7 @@
 #include "ffmpeg_content.h"
 #include "image_content.h"
 #include "sndfile_content.h"
+#include "subrip_content.h"
 #include "util.h"
 
 using std::string;
@@ -39,6 +40,8 @@ content_factory (shared_ptr<const Film> film, cxml::NodePtr node, int version)
                content.reset (new ImageContent (film, node, version));
        } else if (type == "Sndfile") {
                content.reset (new SndfileContent (film, node, version));
+       } else if (type == "SubRip") {
+               content.reset (new SubRipContent (film, node, version));
        }
 
        return content;
@@ -48,11 +51,16 @@ shared_ptr<Content>
 content_factory (shared_ptr<const Film> film, boost::filesystem::path path)
 {
        shared_ptr<Content> content;
+
+       string ext = path.extension().string ();
+       transform (ext.begin(), ext.end(), ext.begin(), ::tolower);
                
        if (valid_image_file (path)) {
                content.reset (new ImageContent (film, path));
        } else if (SndfileContent::valid_file (path)) {
                content.reset (new SndfileContent (film, path));
+       } else if (ext == ".srt") {
+               content.reset (new SubRipContent (film, path));
        } else {
                content.reset (new FFmpegContent (film, path));
        }
index ff32e43f2b67a115673a53268b229618e0815750..f4ebe0dbd18a5ed7236372c352861bf5cca28888 100644 (file)
@@ -20,6 +20,7 @@
 #ifndef DCPOMATIC_LIB_DECODED_H
 #define DCPOMATIC_LIB_DECODED_H
 
+#include <libdcp/subtitle_asset.h>
 #include "types.h"
 #include "rect.h"
 #include "util.h"
@@ -85,16 +86,16 @@ public:
        AudioFrame frame;
 };
 
-class DecodedSubtitle : public Decoded
+class DecodedImageSubtitle : public Decoded
 {
 public:
-       DecodedSubtitle ()
+       DecodedImageSubtitle ()
                : content_time (0)
                , content_time_to (0)
                , dcp_time_to (0)
        {}
 
-       DecodedSubtitle (boost::shared_ptr<Image> im, dcpomatic::Rect<double> r, ContentTime f, ContentTime t)
+       DecodedImageSubtitle (boost::shared_ptr<Image> im, dcpomatic::Rect<double> r, ContentTime f, ContentTime t)
                : image (im)
                , rect (r)
                , content_time (f)
@@ -115,4 +116,30 @@ public:
        DCPTime dcp_time_to;
 };
 
+class DecodedTextSubtitle : public Decoded
+{
+public:
+       DecodedTextSubtitle ()
+               : dcp_time_to (0)
+       {}
+
+       DecodedTextSubtitle (std::list<libdcp::Subtitle> s)
+               : subs (s)
+       {}
+
+       void set_dcp_times (VideoFrame, AudioFrame, FrameRateChange frc, DCPTime offset)
+       {
+               if (subs.empty ()) {
+                       return;
+               }
+
+               /* Assuming that all subs are at the same time */
+               dcp_time = rint (subs.front().in().to_ticks() * 4 * TIME_HZ / frc.speed_up) + offset;
+               dcp_time_to = rint (subs.front().out().to_ticks() * 4 * TIME_HZ / frc.speed_up) + offset;
+       }
+
+       std::list<libdcp::Subtitle> subs;
+       DCPTime dcp_time_to;
+};
+
 #endif
index 8144f41b999690f526db309a4102547f4e69a05c..e05ac4ff04b3fdebb5617129f5c9a0eb6c9dc10e 100644 (file)
@@ -56,8 +56,14 @@ MissingSettingError::MissingSettingError (string s)
 
 }
 
-PixelFormatError::PixelFormatError (std::string o, AVPixelFormat f)
+PixelFormatError::PixelFormatError (string o, AVPixelFormat f)
        : StringError (String::compose (_("Cannot handle pixel format %1 during %2"), f, o))
 {
 
 }
+
+SubRipError::SubRipError (string saw, string expecting, boost::filesystem::path f)
+       : FileError (String::compose (_("Error in SubRip file: saw %1 while expecting %2"), saw, expecting), f)
+{
+
+}
index c1240538f3de2e2a10b6dfb052e17d2bbbbffc9e..61163c8d12403dc492251f3e85668b4fa9a1119e 100644 (file)
@@ -230,6 +230,13 @@ public:
        PixelFormatError (std::string o, AVPixelFormat f);
 };
 
+/** An error that occurs while parsing a SubRip file */
+class SubRipError : public FileError
+{
+public:
+       SubRipError (std::string, std::string, boost::filesystem::path);
+};
+
 /** A parent class for classes which have a need to catch and
  *  re-throw exceptions.  This is intended for classes
  *  which run their own thread; they should do something like
index 52afe2a271e79360cf8fea54257f3e6b239495c9..55f4eb5c69b63cc97c0c5ce6ba1bb44db619b0ff 100644 (file)
@@ -550,7 +550,7 @@ FFmpegDecoder::decode_subtitle_packet ()
           indicate that the previous subtitle should stop.
        */
        if (sub.num_rects <= 0) {
-               subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
+               image_subtitle (shared_ptr<Image> (), dcpomatic::Rect<double> (), 0, 0);
                return;
        } else if (sub.num_rects > 1) {
                throw DecodeError (_("multi-part subtitles not yet supported"));
@@ -559,13 +559,8 @@ FFmpegDecoder::decode_subtitle_packet ()
        /* Subtitle PTS in seconds (within the source, not taking into account any of the
           source that we may have chopped off for the DCP)
        */
-<<<<<<< HEAD
-       double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _pts_offset;
+       double const packet_time = (static_cast<double> (sub.pts) / AV_TIME_BASE) + _pts_offset;
        
-=======
-       double const packet_time = (static_cast<double> (sub.pts ) / AV_TIME_BASE) + _video_pts_offset;
-
->>>>>>> master
        /* hence start time for this sub */
        ContentTime const from = (packet_time + (double (sub.start_display_time) / 1e3)) * TIME_HZ;
        ContentTime const to = (packet_time + (double (sub.end_display_time) / 1e3)) * TIME_HZ;
@@ -603,7 +598,7 @@ FFmpegDecoder::decode_subtitle_packet ()
 
        libdcp::Size const vs = _ffmpeg_content->video_size ();
 
-       subtitle (
+       image_subtitle (
                image,
                dcpomatic::Rect<double> (
                        static_cast<double> (rect->x) / vs.width,
index 77630f0e3c8e4e5d4f442351ae05e876d7d78f00..cb6d519842c5c6cfce1ac92d5f1fe83f74a04e0c 100644 (file)
 #include "sndfile_decoder.h"
 #include "sndfile_content.h"
 #include "subtitle_content.h"
+#include "subrip_decoder.h"
+#include "subrip_content.h"
 #include "playlist.h"
 #include "job.h"
 #include "image.h"
 #include "ratio.h"
 #include "log.h"
 #include "scaler.h"
+#include "render_subtitles.h"
 
 using std::list;
 using std::cout;
@@ -167,7 +170,8 @@ Player::pass ()
 
        shared_ptr<DecodedVideo> dv = dynamic_pointer_cast<DecodedVideo> (earliest_decoded);
        shared_ptr<DecodedAudio> da = dynamic_pointer_cast<DecodedAudio> (earliest_decoded);
-       shared_ptr<DecodedSubtitle> ds = dynamic_pointer_cast<DecodedSubtitle> (earliest_decoded);
+       shared_ptr<DecodedImageSubtitle> dis = dynamic_pointer_cast<DecodedImageSubtitle> (earliest_decoded);
+       shared_ptr<DecodedTextSubtitle> dts = dynamic_pointer_cast<DecodedTextSubtitle> (earliest_decoded);
 
        /* Will be set to false if we shouldn't consume the peeked DecodedThing */
        bool consume = true;
@@ -231,10 +235,14 @@ Player::pass ()
                        _statistics.audio.skip += da->data->frames();
                }
                
-       } else if (ds && _video) {
-               _in_subtitle.piece = earliest_piece;
-               _in_subtitle.subtitle = ds;
-               update_subtitle ();
+       } else if (dis && _video) {
+               _image_subtitle.piece = earliest_piece;
+               _image_subtitle.subtitle = dis;
+               update_subtitle_from_image ();
+       } else if (dts && _video) {
+               _text_subtitle.piece = earliest_piece;
+               _text_subtitle.subtitle = dts;
+               update_subtitle_from_text ();
        }
 
        if (consume) {
@@ -445,12 +453,38 @@ Player::setup_pieces ()
                shared_ptr<Decoder> decoder;
                optional<FrameRateChange> frc;
 
+               /* Work out a FrameRateChange for the best overlap video for this content, in case we need it below */
+               DCPTime best_overlap_t = 0;
+               shared_ptr<VideoContent> best_overlap;
+               for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
+                       shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
+                       if (!vc) {
+                               continue;
+                       }
+                       
+                       DCPTime const overlap = max (vc->position(), (*i)->position()) - min (vc->end(), (*i)->end());
+                       if (overlap > best_overlap_t) {
+                               best_overlap = vc;
+                               best_overlap_t = overlap;
+                       }
+               }
+
+               optional<FrameRateChange> best_overlap_frc;
+               if (best_overlap) {
+                       best_overlap_frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
+               } else {
+                       /* No video overlap; e.g. if the DCP is just audio */
+                       best_overlap_frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
+               }
+
+               /* FFmpeg */
                shared_ptr<const FFmpegContent> fc = dynamic_pointer_cast<const FFmpegContent> (*i);
                if (fc) {
                        decoder.reset (new FFmpegDecoder (_film, fc, _video, _audio));
                        frc = FrameRateChange (fc->video_frame_rate(), _film->video_frame_rate());
                }
-               
+
+               /* ImageContent */
                shared_ptr<const ImageContent> ic = dynamic_pointer_cast<const ImageContent> (*i);
                if (ic) {
                        /* See if we can re-use an old ImageDecoder */
@@ -468,36 +502,18 @@ Player::setup_pieces ()
                        frc = FrameRateChange (ic->video_frame_rate(), _film->video_frame_rate());
                }
 
+               /* SndfileContent */
                shared_ptr<const SndfileContent> sc = dynamic_pointer_cast<const SndfileContent> (*i);
                if (sc) {
                        decoder.reset (new SndfileDecoder (_film, sc));
+                       frc = best_overlap_frc;
+               }
 
-                       /* Working out the frc for this content is a bit tricky: what if it overlaps
-                          two pieces of video content with different frame rates?  For now, use
-                          the one with the best overlap.
-                       */
-
-                       DCPTime best_overlap_t = 0;
-                       shared_ptr<VideoContent> best_overlap;
-                       for (ContentList::iterator j = content.begin(); j != content.end(); ++j) {
-                               shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (*j);
-                               if (!vc) {
-                                       continue;
-                               }
-
-                               DCPTime const overlap = max (vc->position(), sc->position()) - min (vc->end(), sc->end());
-                               if (overlap > best_overlap_t) {
-                                       best_overlap = vc;
-                                       best_overlap_t = overlap;
-                               }
-                       }
-
-                       if (best_overlap) {
-                               frc = FrameRateChange (best_overlap->video_frame_rate(), _film->video_frame_rate ());
-                       } else {
-                               /* No video overlap; e.g. if the DCP is just audio */
-                               frc = FrameRateChange (_film->video_frame_rate(), _film->video_frame_rate ());
-                       }
+               /* SubRipContent */
+               shared_ptr<const SubRipContent> rc = dynamic_pointer_cast<const SubRipContent> (*i);
+               if (rc) {
+                       decoder.reset (new SubRipDecoder (_film, rc));
+                       frc = best_overlap_frc;
                }
 
                ContentTime st = (*i)->trim_start() * frc->speed_up;
@@ -533,7 +549,8 @@ Player::content_changed (weak_ptr<Content> w, int property, bool frequent)
 
        } else if (property == SubtitleContentProperty::SUBTITLE_OFFSET || property == SubtitleContentProperty::SUBTITLE_SCALE) {
 
-               update_subtitle ();
+               update_subtitle_from_image ();
+               update_subtitle_from_text ();
                Changed (frequent);
 
        } else if (
@@ -616,14 +633,14 @@ Player::film_changed (Film::Property p)
 }
 
 void
-Player::update_subtitle ()
+Player::update_subtitle_from_image ()
 {
-       shared_ptr<Piece> piece = _in_subtitle.piece.lock ();
+       shared_ptr<Piece> piece = _image_subtitle.piece.lock ();
        if (!piece) {
                return;
        }
 
-       if (!_in_subtitle.subtitle->image) {
+       if (!_image_subtitle.subtitle->image) {
                _out_subtitle.image.reset ();
                return;
        }
@@ -631,7 +648,7 @@ Player::update_subtitle ()
        shared_ptr<SubtitleContent> sc = dynamic_pointer_cast<SubtitleContent> (piece->content);
        assert (sc);
 
-       dcpomatic::Rect<double> in_rect = _in_subtitle.subtitle->rect;
+       dcpomatic::Rect<double> in_rect = _image_subtitle.subtitle->rect;
        libdcp::Size scaled_size;
 
        in_rect.y += sc->subtitle_offset ();
@@ -654,30 +671,16 @@ Player::update_subtitle ()
        
        _out_subtitle.position.x = rint (_video_container_size.width * (in_rect.x + (in_rect.width * (1 - sc->subtitle_scale ()) / 2)));
        _out_subtitle.position.y = rint (_video_container_size.height * (in_rect.y + (in_rect.height * (1 - sc->subtitle_scale ()) / 2)));
-
-       _out_subtitle.image = _in_subtitle.subtitle->image->scale (
+       
+       _out_subtitle.image = _image_subtitle.subtitle->image->scale (
                scaled_size,
                Scaler::from_id ("bicubic"),
-               PIX_FMT_RGBA,
+               _image_subtitle.subtitle->image->pixel_format (),
                true
                );
-
-<<<<<<< HEAD
-       _out_subtitle.from = _in_subtitle.subtitle->dcp_time;
-       _out_subtitle.to = _in_subtitle.subtitle->dcp_time_to;
-=======
-       /* XXX: hack */
-       Time from = _in_subtitle.from;
-       Time to = _in_subtitle.to;
-       shared_ptr<VideoContent> vc = dynamic_pointer_cast<VideoContent> (piece->content);
-       if (vc) {
-               from = rint (from * vc->video_frame_rate() / _film->video_frame_rate());
-               to = rint (to * vc->video_frame_rate() / _film->video_frame_rate());
-       }
        
-       _out_subtitle.from = from * piece->content->position ();
-       _out_subtitle.to = to + piece->content->position ();
->>>>>>> master
+       _out_subtitle.from = _image_subtitle.subtitle->dcp_time;
+       _out_subtitle.to = _image_subtitle.subtitle->dcp_time_to;
 }
 
 /** Re-emit the last frame that was emitted, using current settings for crop, ratio, scaler and subtitles.
@@ -698,13 +701,23 @@ Player::repeat_last_video ()
        return true;
 }
 
+void
+Player::update_subtitle_from_text ()
+{
+       if (_text_subtitle.subtitle->subs.empty ()) {
+               _out_subtitle.image.reset ();
+               return;
+       }
+
+       render_subtitles (_text_subtitle.subtitle->subs, _video_container_size, _out_subtitle.image, _out_subtitle.position);
+}
+
 void
 Player::set_approximate_size ()
 {
        _approximate_size = true;
 }
                              
-
 PlayerImage::PlayerImage (
        shared_ptr<const Image> in,
        Crop crop,
index 5f7c553a0f9f0bd44b2c6b49c628c59520d9381c..377e8bd18e5ae3634d4faa0848f19fbcee088082 100644 (file)
@@ -23,6 +23,7 @@
 #include <list>
 #include <boost/shared_ptr.hpp>
 #include <boost/enable_shared_from_this.hpp>
+#include <libdcp/subtitle_asset.h>
 #include "playlist.h"
 #include "content.h"
 #include "film.h"
@@ -151,7 +152,8 @@ private:
        void emit_black ();
        void emit_silence (AudioFrame);
        void film_changed (Film::Property);
-       void update_subtitle ();
+       void update_subtitle_from_image ();
+       void update_subtitle_from_text ();
        void emit_video (boost::weak_ptr<Piece>, boost::shared_ptr<DecodedVideo>);
        void emit_audio (boost::weak_ptr<Piece>, boost::shared_ptr<DecodedAudio>);
        void step_video_position (boost::shared_ptr<DecodedVideo>);
@@ -178,9 +180,14 @@ private:
 
        struct {
                boost::weak_ptr<Piece> piece;
-               boost::shared_ptr<DecodedSubtitle> subtitle;
-       } _in_subtitle;
+               boost::shared_ptr<DecodedImageSubtitle> subtitle;
+       } _image_subtitle;
 
+       struct {
+               boost::weak_ptr<Piece> piece;
+               boost::shared_ptr<DecodedTextSubtitle> subtitle;
+       } _text_subtitle;
+       
        struct {
                Position<int> position;
                boost::shared_ptr<Image> image;
diff --git a/src/lib/render_subtitles.cc b/src/lib/render_subtitles.cc
new file mode 100644 (file)
index 0000000..6306817
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <cairomm/cairomm.h>
+#include <pangomm.h>
+#include "render_subtitles.h"
+#include "types.h"
+#include "image.h"
+
+using std::list;
+using std::cout;
+using std::string;
+using std::min;
+using std::max;
+using boost::shared_ptr;
+using boost::optional;
+
+static int
+calculate_position (libdcp::VAlign v_align, double v_position, int target_height, int offset)
+{
+       switch (v_align) {
+       case libdcp::TOP:
+               return (v_position / 100) * target_height - offset;
+       case libdcp::CENTER:
+               return (0.5 + v_position / 100) * target_height - offset;
+       case libdcp::BOTTOM:
+               return (1.0 - v_position / 100) * target_height - offset;
+       }
+
+       return 0;
+}
+
+void
+render_subtitles (list<libdcp::Subtitle> subtitles, libdcp::Size target, shared_ptr<Image>& image, Position<int>& position)
+{
+       if (subtitles.empty ()) {
+               image.reset ();
+               return;
+       }
+
+       /* Estimate height that the subtitle image needs to be */
+       optional<int> top;
+       optional<int> bottom;
+       for (list<libdcp::Subtitle>::const_iterator i = subtitles.begin(); i != subtitles.end(); ++i) {
+               int const b = calculate_position (i->v_align(), i->v_position(), target.height, 0);
+               int const t = b - i->size() * target.height / (11 * 72);
+
+               top = min (top.get_value_or (t), t);
+               bottom = max (bottom.get_value_or (b), b);
+       }
+
+       top = top.get() - 32;
+       bottom = bottom.get() + 32;
+
+       image.reset (new Image (PIX_FMT_RGBA, libdcp::Size (target.width, bottom.get() - top.get ()), false));
+       image->make_black ();
+
+       Cairo::RefPtr<Cairo::ImageSurface> surface = Cairo::ImageSurface::create (
+               image->data()[0],
+               Cairo::FORMAT_ARGB32,
+               image->size().width,
+               image->size().height,
+               Cairo::ImageSurface::format_stride_for_width (Cairo::FORMAT_ARGB32, image->size().width)
+               );
+       
+       Cairo::RefPtr<Cairo::Context> context = Cairo::Context::create (surface);
+       Glib::RefPtr<Pango::Layout> layout = Pango::Layout::create (context);
+
+       layout->set_width (image->size().width * PANGO_SCALE);
+       layout->set_alignment (Pango::ALIGN_CENTER);
+
+       context->set_line_width (1);
+
+       for (list<libdcp::Subtitle>::const_iterator i = subtitles.begin(); i != subtitles.end(); ++i) {
+               string f = i->font ();
+               if (f.empty ()) {
+                       f = "Arial";
+               }
+               Pango::FontDescription font (f);
+               font.set_absolute_size (i->size_in_pixels (target.height) * PANGO_SCALE);
+               if (i->italic ()) {
+                       font.set_style (Pango::STYLE_ITALIC);
+               }
+               layout->set_font_description (font);
+               layout->set_text (i->text ());
+
+               /* Compute fade factor */
+               /* XXX */
+               float fade_factor = 1;
+#if 0          
+               libdcp::Time now (time * 1000 / (4 * TIME_HZ));
+               libdcp::Time end_fade_up = i->in() + i->fade_up_time ();
+               libdcp::Time start_fade_down = i->out() - i->fade_down_time ();
+               if (now < end_fade_up) {
+                       fade_factor = (now - i->in()) / i->fade_up_time();
+               } else if (now > start_fade_down) {
+                       fade_factor = 1.0 - ((now - start_fade_down) / i->fade_down_time ());
+               }
+#endif         
+
+               layout->update_from_cairo_context (context);
+               
+               /* Work out position */
+
+               int const x = 0;
+               int const y = calculate_position (i->v_align (), i->v_position (), target.height, (layout->get_baseline() / PANGO_SCALE) + top.get ());
+
+               if (i->effect() == libdcp::SHADOW) {
+                       /* Drop-shadow effect */
+                       libdcp::Color const ec = i->effect_color ();
+                       context->set_source_rgba (float(ec.r) / 255, float(ec.g) / 255, float(ec.b) / 255, fade_factor);
+                       context->move_to (x + 4, y + 4);
+                       layout->add_to_cairo_context (context);
+                       context->fill ();
+               }
+
+               /* The actual subtitle */
+               context->move_to (x, y);
+               libdcp::Color const c = i->color ();
+               context->set_source_rgba (float(c.r) / 255, float(c.g) / 255, float(c.b) / 255, fade_factor);
+               layout->add_to_cairo_context (context);
+               context->fill ();
+
+               if (i->effect() == libdcp::BORDER) {
+                       /* Border effect */
+                       context->move_to (x, y);
+                       libdcp::Color ec = i->effect_color ();
+                       context->set_source_rgba (float(ec.r) / 255, float(ec.g) / 255, float(ec.b) / 255, fade_factor);
+                       layout->add_to_cairo_context (context);
+                       context->stroke ();
+               }
+       }
+}
+
diff --git a/src/lib/render_subtitles.h b/src/lib/render_subtitles.h
new file mode 100644 (file)
index 0000000..2264838
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <libdcp/subtitle_asset.h>
+#include <libdcp/util.h>
+#include "position.h"
+
+class Image;
+
+void
+render_subtitles (std::list<libdcp::Subtitle>, libdcp::Size, boost::shared_ptr<Image> &, Position<int> &);
diff --git a/src/lib/subrip.cc b/src/lib/subrip.cc
new file mode 100644 (file)
index 0000000..380a2ce
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <boost/algorithm/string.hpp>
+#include "subrip.h"
+#include "subrip_content.h"
+#include "subrip_subtitle.h"
+#include "cross.h"
+#include "exceptions.h"
+
+#include "i18n.h"
+
+using std::string;
+using std::list;
+using std::vector;
+using std::cout;
+using boost::shared_ptr;
+using boost::lexical_cast;
+using boost::algorithm::trim;
+
+SubRip::SubRip (shared_ptr<const SubRipContent> content)
+{
+       FILE* f = fopen_boost (content->path (0), "r");
+       if (!f) {
+               throw OpenFileError (content->path (0));
+       }
+
+       enum {
+               COUNTER,
+               METADATA,
+               CONTENT
+       } state = COUNTER;
+
+       char buffer[256];
+       int next_count = 1;
+
+       boost::optional<SubRipSubtitle> current;
+       list<string> lines;
+       
+       while (!feof (f)) {
+               fgets (buffer, sizeof (buffer), f);
+               if (feof (f)) {
+                       break;
+               }
+               
+               string line (buffer);
+               trim_right_if (line, boost::is_any_of ("\n\r"));
+               
+               switch (state) {
+               case COUNTER:
+               {
+                       int x = 0;
+                       try {
+                               x = lexical_cast<int> (line);
+                       } catch (...) {
+
+                       }
+                       
+                       if (x == next_count) {
+                               state = METADATA;
+                               ++next_count;
+                               current = SubRipSubtitle ();
+                       } else {
+                               throw SubRipError (line, _("a subtitle count"), content->path (0));
+                       }
+               }
+               break;
+               case METADATA:
+               {
+                       vector<string> p;
+                       boost::algorithm::split (p, line, boost::algorithm::is_any_of (" "));
+                       if (p.size() != 3 && p.size() != 7) {
+                               throw SubRipError (line, _("a time/position line"), content->path (0));
+                       }
+
+                       current->from = convert_time (p[0]);
+                       current->to = convert_time (p[2]);
+
+                       if (p.size() > 3) {
+                               current->x1 = convert_coordinate (p[3]);
+                               current->x2 = convert_coordinate (p[4]);
+                               current->y1 = convert_coordinate (p[5]);
+                               current->y2 = convert_coordinate (p[6]);
+                       }
+                       state = CONTENT;
+                       break;
+               }
+               case CONTENT:
+                       if (line.empty ()) {
+                               state = COUNTER;
+                               current->pieces = convert_content (lines);
+                               _subtitles.push_back (current.get ());
+                               current.reset ();
+                               lines.clear ();
+                       } else {
+                               lines.push_back (line);
+                       }
+                       break;
+               }
+       }
+
+       if (state == CONTENT) {
+               current->pieces = convert_content (lines);
+               _subtitles.push_back (current.get ());
+       }
+
+       fclose (f);
+}
+
+ContentTime
+SubRip::convert_time (string t)
+{
+       ContentTime r = 0;
+
+       vector<string> a;
+       boost::algorithm::split (a, t, boost::is_any_of (":"));
+       assert (a.size() == 3);
+       r += lexical_cast<int> (a[0]) * 60 * 60 * TIME_HZ;
+       r += lexical_cast<int> (a[1]) * 60 * TIME_HZ;
+
+       vector<string> b;
+       boost::algorithm::split (b, a[2], boost::is_any_of (","));
+       r += lexical_cast<int> (b[0]) * TIME_HZ;
+       r += lexical_cast<int> (b[1]) * TIME_HZ / 1000;
+
+       return r;
+}
+
+int
+SubRip::convert_coordinate (string t)
+{
+       vector<string> a;
+       boost::algorithm::split (a, t, boost::is_any_of (":"));
+       assert (a.size() == 2);
+       return lexical_cast<int> (a[1]);
+}
+
+void
+SubRip::maybe_content (list<SubRipSubtitlePiece>& pieces, SubRipSubtitlePiece& p)
+{
+       if (!p.text.empty ()) {
+               pieces.push_back (p);
+               p.text.clear ();
+       }
+}
+
+list<SubRipSubtitlePiece>
+SubRip::convert_content (list<string> t)
+{
+       list<SubRipSubtitlePiece> pieces;
+       
+       SubRipSubtitlePiece p;
+
+       enum {
+               TEXT,
+               TAG
+       } state = TEXT;
+
+       string tag;
+
+       /* XXX: missing <font> support */
+       /* XXX: nesting of tags e.g. <b>foo<i>bar<b>baz</b>fred</i>jim</b> might
+          not work, I think.
+       */
+
+       for (list<string>::const_iterator i = t.begin(); i != t.end(); ++i) {
+               for (size_t j = 0; j < i->size(); ++j) {
+                       switch (state) {
+                       case TEXT:
+                               if ((*i)[j] == '<' || (*i)[j] == '{') {
+                                       state = TAG;
+                               } else {
+                                       p.text += (*i)[j];
+                               }
+                               break;
+                       case TAG:
+                               if ((*i)[j] == '>' || (*i)[j] == '}') {
+                                       if (tag == "b") {
+                                               maybe_content (pieces, p);
+                                               p.bold = true;
+                                       } else if (tag == "/b") {
+                                               maybe_content (pieces, p);
+                                               p.bold = false;
+                                       } else if (tag == "i") {
+                                               maybe_content (pieces, p);
+                                               p.italic = true;
+                                       } else if (tag == "/i") {
+                                               maybe_content (pieces, p);
+                                               p.italic = false;
+                                       } else if (tag == "u") {
+                                               maybe_content (pieces, p);
+                                               p.underline = true;
+                                       } else if (tag == "/u") {
+                                               maybe_content (pieces, p);
+                                               p.underline = false;
+                                       }
+                                       tag.clear ();
+                                       state = TEXT;
+                               } else {
+                                       tag += (*i)[j];
+                               }
+                               break;
+                       }
+               }
+       }
+
+       maybe_content (pieces, p);
+
+       return pieces;
+}
+
+ContentTime
+SubRip::length () const
+{
+       if (_subtitles.empty ()) {
+               return 0;
+       }
+
+       return _subtitles.back().to;
+}
diff --git a/src/lib/subrip.h b/src/lib/subrip.h
new file mode 100644 (file)
index 0000000..e7d2167
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef DCPOMATIC_SUBRIP_H
+#define DCPOMATIC_SUBRIP_H
+
+#include "subrip_subtitle.h"
+
+class SubRipContent;
+class subrip_time_test;
+class subrip_coordinate_test;
+class subrip_content_test;
+class subrip_parse_test;
+
+class SubRip
+{
+public:
+       SubRip (boost::shared_ptr<const SubRipContent>);
+
+       ContentTime length () const;
+
+protected:
+       std::vector<SubRipSubtitle> _subtitles;
+       
+private:
+       friend class subrip_time_test;
+       friend class subrip_coordinate_test;
+       friend class subrip_content_test;
+       friend class subrip_parse_test;
+       
+       static ContentTime convert_time (std::string);
+       static int convert_coordinate (std::string);
+       static std::list<SubRipSubtitlePiece> convert_content (std::list<std::string>);
+       static void maybe_content (std::list<SubRipSubtitlePiece> &, SubRipSubtitlePiece &);
+};
+
+#endif
diff --git a/src/lib/subrip_content.cc b/src/lib/subrip_content.cc
new file mode 100644 (file)
index 0000000..48d3528
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "subrip_content.h"
+#include "util.h"
+#include "subrip.h"
+
+#include "i18n.h"
+
+using std::stringstream;
+using std::string;
+using boost::shared_ptr;
+
+SubRipContent::SubRipContent (shared_ptr<const Film> film, boost::filesystem::path path)
+       : Content (film, path)
+       , SubtitleContent (film, path)
+{
+
+}
+
+SubRipContent::SubRipContent (shared_ptr<const Film> film, shared_ptr<const cxml::Node> node, int)
+       : Content (film, node)
+       , SubtitleContent (film, node)
+{
+
+}
+
+void
+SubRipContent::examine (boost::shared_ptr<Job> job)
+{
+       Content::examine (job);
+       SubRip s (shared_from_this ());
+       boost::mutex::scoped_lock lm (_mutex);
+       _length = s.length ();
+}
+
+string
+SubRipContent::summary () const
+{
+       return path_summary() + " " + _("[subtitles]");
+}
+
+string
+SubRipContent::technical_summary () const
+{
+       return Content::technical_summary() + " - " + _("SubRip subtitles");
+}
+
+string
+SubRipContent::information () const
+{
+       
+}
+       
+void
+SubRipContent::as_xml (xmlpp::Node* node)
+{
+       node->add_child("Type")->add_child_text ("SubRip");
+       Content::as_xml (node);
+       SubtitleContent::as_xml (node);
+}
+
+DCPTime
+SubRipContent::full_length () const
+{
+       /* XXX: this assumes that the timing of the SubRip file is appropriate
+          for the DCP's frame rate.
+       */
+       return _length;
+}
+
+string
+SubRipContent::identifier () const
+{
+       LocaleGuard lg;
+
+       stringstream s;
+       s << Content::identifier()
+         << "_" << subtitle_scale()
+         << "_" << subtitle_offset();
+
+       return s.str ();
+}
diff --git a/src/lib/subrip_content.h b/src/lib/subrip_content.h
new file mode 100644 (file)
index 0000000..6138c04
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "subtitle_content.h"
+
+class SubRipContent : public SubtitleContent
+{
+public:
+       SubRipContent (boost::shared_ptr<const Film>, boost::filesystem::path);
+       SubRipContent (boost::shared_ptr<const Film>, boost::shared_ptr<const cxml::Node>, int);
+
+       boost::shared_ptr<SubRipContent> shared_from_this () {
+               return boost::dynamic_pointer_cast<SubRipContent> (Content::shared_from_this ());
+       }
+       
+       void examine (boost::shared_ptr<Job>);
+       std::string summary () const;
+       std::string technical_summary () const;
+       std::string information () const;
+       void as_xml (xmlpp::Node *);
+       DCPTime full_length () const;
+       std::string identifier () const;
+
+private:
+       DCPTime _length;
+};
diff --git a/src/lib/subrip_decoder.cc b/src/lib/subrip_decoder.cc
new file mode 100644 (file)
index 0000000..aecee4e
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include "subrip_decoder.h"
+
+using std::list;
+using boost::shared_ptr;
+
+SubRipDecoder::SubRipDecoder (shared_ptr<const Film> film, shared_ptr<const SubRipContent> content)
+       : Decoder (film)
+       , SubtitleDecoder (film)
+       , SubRip (content)
+       , _next (0)
+{
+
+}
+
+bool
+SubRipDecoder::pass ()
+{
+       if (_next >= _subtitles.size ()) {
+               return true;
+       }
+       
+       list<libdcp::Subtitle> out;
+       for (list<SubRipSubtitlePiece>::const_iterator i = _subtitles[_next].pieces.begin(); i != _subtitles[_next].pieces.end(); ++i) {
+               out.push_back (
+                       libdcp::Subtitle (
+                               "Arial",
+                               i->italic,
+                               libdcp::Color (255, 255, 255),
+                               72,
+                               _subtitles[_next].from,
+                               _subtitles[_next].to,
+                               0.9,
+                               libdcp::BOTTOM,
+                               i->text,
+                               libdcp::NONE,
+                               libdcp::Color (255, 255, 255),
+                               0,
+                               0
+                               )
+                       );
+       }
+
+       text_subtitle (out);
+       _next++;
+       return false;
+}
diff --git a/src/lib/subrip_decoder.h b/src/lib/subrip_decoder.h
new file mode 100644 (file)
index 0000000..26d5d50
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef DCPOMATIC_SUBRIP_DECODER_H
+#define DCPOMATIC_SUBRIP_DECODER_H
+
+#include "subtitle_decoder.h"
+#include "subrip.h"
+
+class SubRipContent;
+
+class SubRipDecoder : public SubtitleDecoder, public SubRip
+{
+public:
+       SubRipDecoder (boost::shared_ptr<const Film>, boost::shared_ptr<const SubRipContent>);
+       
+       bool pass ();
+
+private:
+       size_t _next;
+};
+
+#endif
diff --git a/src/lib/subrip_subtitle.h b/src/lib/subrip_subtitle.h
new file mode 100644 (file)
index 0000000..f730ee4
--- /dev/null
@@ -0,0 +1,58 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#ifndef DCPOMATIC_SUBRIP_SUBTITLE_H
+#define DCPOMATIC_SUBRIP_SUBTITLE_H
+
+#include <boost/optional.hpp>
+#include <libdcp/types.h>
+#include "types.h"
+
+struct SubRipSubtitlePiece
+{
+       SubRipSubtitlePiece ()
+               : bold (false)
+               , italic (false)
+               , underline (false)
+       {}
+       
+       std::string text;
+       bool bold;
+       bool italic;
+       bool underline;
+       libdcp::Color color;
+};
+
+struct SubRipSubtitle
+{
+       SubRipSubtitle ()
+               : from (0)
+               , to (0)
+       {}
+       
+       ContentTime from;
+       ContentTime to;
+       boost::optional<int> x1;
+       boost::optional<int> x2;
+       boost::optional<int> y1;
+       boost::optional<int> y2;
+       std::list<SubRipSubtitlePiece> pieces;
+};
+
+#endif
index 7ba969933d077717ed125546657767d8fcc1aeda..e5cadb7b48b4e513c3b11a7ad47babc10ecda689 100644 (file)
@@ -20,6 +20,7 @@
 #include <boost/shared_ptr.hpp>
 #include "subtitle_decoder.h"
 
+using std::list;
 using boost::shared_ptr;
 using boost::optional;
 
@@ -30,11 +31,17 @@ SubtitleDecoder::SubtitleDecoder (shared_ptr<const Film> f)
 }
 
 
-/** Called by subclasses when a subtitle is ready.
+/** Called by subclasses when an image subtitle is ready.
  *  Image may be 0 to say that there is no current subtitle.
  */
 void
-SubtitleDecoder::subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, ContentTime from, ContentTime to)
+SubtitleDecoder::image_subtitle (shared_ptr<Image> image, dcpomatic::Rect<double> rect, ContentTime from, ContentTime to)
 {
-       _pending.push_back (shared_ptr<DecodedSubtitle> (new DecodedSubtitle (image, rect, from, to)));
+       _pending.push_back (shared_ptr<DecodedImageSubtitle> (new DecodedImageSubtitle (image, rect, from, to)));
+}
+
+void
+SubtitleDecoder::text_subtitle (list<libdcp::Subtitle> s)
+{
+       _pending.push_back (shared_ptr<DecodedTextSubtitle> (new DecodedTextSubtitle (s)));
 }
index fd1d71f33fdef23b3ef8ed7b5d69140baa4ed2cb..82662d192fbd1295d8574d7001b44d413984c353 100644 (file)
 
 */
 
+#ifndef DCPOMATIC_SUBTITLE_DECODER_H
+#define DCPOMATIC_SUBTITLE_DECODER_H
+
 #include <boost/signals2.hpp>
+#include <libdcp/subtitle_asset.h>
 #include "decoder.h"
 #include "rect.h"
 #include "types.h"
@@ -33,5 +37,8 @@ public:
        SubtitleDecoder (boost::shared_ptr<const Film>);
 
 protected:
-       void subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, ContentTime, ContentTime);
+       void image_subtitle (boost::shared_ptr<Image>, dcpomatic::Rect<double>, ContentTime, ContentTime);
+       void text_subtitle (std::list<libdcp::Subtitle>);
 };
+
+#endif
index a7dac9013a3cef53857ccd135b30b0df5c84a3e1..ef203c2bdc82fae52119f0acef4b4d4c2ba27764 100644 (file)
@@ -47,6 +47,7 @@
 #include <openssl/md5.h>
 #include <magick/MagickCore.h>
 #include <magick/version.h>
+#include <pangomm/init.h>
 #include <libdcp/version.h>
 #include <libdcp/util.h>
 #include <libdcp/signer_chain.h>
@@ -309,6 +310,7 @@ dcpomatic_setup ()
        setenv ("LTDL_LIBRARY_PATH", lib.c_str (), 1);
 #endif 
 
+       Pango::init ();
        libdcp::init ();
        
        Ratio::setup_ratios ();
index 81a55a160bc42ab200931e9bec862d712ab7d965..8a20618c0641c91e30b82bdce72047926478ba3c 100644 (file)
@@ -41,6 +41,7 @@ sources = """
           player.cc
           playlist.cc
           ratio.cc
+          render_subtitles.cc
           resampler.cc
           scp_dcp_job.cc
           scaler.cc
@@ -50,6 +51,9 @@ sources = """
           sndfile_content.cc
           sndfile_decoder.cc
           sound_processor.cc
+          subrip.cc
+          subrip_content.cc
+          subrip_decoder.cc
           subtitle_content.cc
           subtitle_decoder.cc
           timer.cc
@@ -76,7 +80,7 @@ def build(bld):
                  AVCODEC AVUTIL AVFORMAT AVFILTER SWSCALE SWRESAMPLE 
                  BOOST_FILESYSTEM BOOST_THREAD BOOST_DATETIME BOOST_SIGNALS2
                  SNDFILE OPENJPEG POSTPROC TIFF MAGICK SSH DCP CXML GLIB LZMA XML++
-                 CURL ZIP QUICKMAIL
+                 CURL ZIP QUICKMAIL PANGOMM CAIROMM
                  """
 
     obj.source = sources + ' version.cc'
index 45c423355e3cc7578de2ef51a7d407045f94b1dc..f246e99e90e8f1633819af11c29839aa9ae8bbc9 100644 (file)
@@ -841,7 +841,7 @@ FilmEditor::setup_content_sensitivity ()
 
        _video_panel->Enable    (video_selection.size() > 0 && _generally_sensitive);
        _audio_panel->Enable    (audio_selection.size() > 0 && _generally_sensitive);
-       _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<FFmpegContent> (selection.front()) && _generally_sensitive);
+       _subtitle_panel->Enable (selection.size() == 1 && dynamic_pointer_cast<SubtitleContent> (selection.front()) && _generally_sensitive);
        _timing_panel->Enable   (selection.size() == 1 && _generally_sensitive);
 }
 
index 082a6ff11eed1b651b451b4bbb537ee2e84674bd..f3cbb504f0b874b9d5ef97b65d681b2e91ff33f3 100644 (file)
@@ -42,7 +42,7 @@ BOOST_AUTO_TEST_CASE (ratio_test)
 
        Ratio const * r = Ratio::from_id ("119");
        BOOST_CHECK (r);
-       BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), libdcp::Size (2048, 1080)), libdcp::Size (1285, 1080));
+       BOOST_CHECK_EQUAL (fit_ratio_within (r->ratio(), libdcp::Size (2048, 1080)), libdcp::Size (1290, 1080));
 
        r = Ratio::from_id ("133");
        BOOST_CHECK (r);
diff --git a/test/subrip_test.cc b/test/subrip_test.cc
new file mode 100644 (file)
index 0000000..12a77c1
--- /dev/null
@@ -0,0 +1,196 @@
+/*
+    Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+
+    This program 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.
+
+    This program 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 this program; if not, write to the Free Software
+    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+*/
+
+#include <boost/test/unit_test.hpp>
+#include <libdcp/subtitle_asset.h>
+#include "lib/subrip.h"
+#include "lib/subrip_content.h"
+#include "lib/subrip_decoder.h"
+#include "lib/render_subtitles.h"
+#include "test.h"
+
+using std::list;
+using std::vector;
+using std::string;
+using boost::shared_ptr;
+using boost::dynamic_pointer_cast;
+
+/** Test SubRip::convert_time */
+BOOST_AUTO_TEST_CASE (subrip_time_test)
+{
+       BOOST_CHECK_EQUAL (SubRip::convert_time ("00:03:10,500"), rint (((3 * 60) + 10 + 0.5) * TIME_HZ));
+       BOOST_CHECK_EQUAL (SubRip::convert_time ("04:19:51,782"), rint (((4 * 3600) + (19 * 60) + 51 + 0.782) * TIME_HZ));
+}
+
+/** Test SubRip::convert_coordinate */
+BOOST_AUTO_TEST_CASE (subrip_coordinate_test)
+{
+       BOOST_CHECK_EQUAL (SubRip::convert_coordinate ("foo:42"), 42);
+       BOOST_CHECK_EQUAL (SubRip::convert_coordinate ("X1:999"), 999);
+}
+
+/** Test SubRip::convert_content */
+BOOST_AUTO_TEST_CASE (subrip_content_test)
+{
+       list<string> c;
+       list<SubRipSubtitlePiece> p;
+       
+       c.push_back ("Hello world");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       c.clear ();
+
+       c.push_back ("<b>Hello world</b>");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       BOOST_CHECK_EQUAL (p.front().bold, true);
+       c.clear ();
+
+       c.push_back ("<i>Hello world</i>");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       BOOST_CHECK_EQUAL (p.front().italic, true);
+       c.clear ();
+
+       c.push_back ("<u>Hello world</u>");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       BOOST_CHECK_EQUAL (p.front().underline, true);
+       c.clear ();
+
+       c.push_back ("{b}Hello world{/b}");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       BOOST_CHECK_EQUAL (p.front().bold, true);
+       c.clear ();
+
+       c.push_back ("{i}Hello world{/i}");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       BOOST_CHECK_EQUAL (p.front().italic, true);
+       c.clear ();
+
+       c.push_back ("{u}Hello world{/u}");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 1);
+       BOOST_CHECK_EQUAL (p.front().text, "Hello world");
+       BOOST_CHECK_EQUAL (p.front().underline, true);
+       c.clear ();
+
+       c.push_back ("<b>This is <i>nesting</i> of subtitles</b>");
+       p = SubRip::convert_content (c);
+       BOOST_CHECK_EQUAL (p.size(), 3);
+       list<SubRipSubtitlePiece>::iterator i = p.begin ();     
+       BOOST_CHECK_EQUAL (i->text, "This is ");
+       BOOST_CHECK_EQUAL (i->bold, true);
+       BOOST_CHECK_EQUAL (i->italic, false);
+       ++i;
+       BOOST_CHECK_EQUAL (i->text, "nesting");
+       BOOST_CHECK_EQUAL (i->bold, true);
+       BOOST_CHECK_EQUAL (i->italic, true);
+       ++i;
+       BOOST_CHECK_EQUAL (i->text, " of subtitles");
+       BOOST_CHECK_EQUAL (i->bold, true);
+       BOOST_CHECK_EQUAL (i->italic, false);
+       ++i;
+       c.clear ();
+}
+
+/** Test parsing of full SubRip file content */
+BOOST_AUTO_TEST_CASE (subrip_parse_test)
+{
+       shared_ptr<SubRipContent> content (new SubRipContent (shared_ptr<Film> (), "test/data/subrip.srt"));
+       content->examine (shared_ptr<Job> ());
+       BOOST_CHECK_EQUAL (content->full_length(), ((3 * 60) + 56.471) * TIME_HZ);
+
+       SubRip s (content);
+
+       vector<SubRipSubtitle>::const_iterator i = s._subtitles.begin();
+
+       BOOST_CHECK (i != s._subtitles.end ());
+       BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 49.200) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 52.351) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->pieces.size(), 1);
+       BOOST_CHECK_EQUAL (i->pieces.front().text, "This is a subtitle, and it goes over two lines.");
+
+       ++i;
+       BOOST_CHECK (i != s._subtitles.end ());
+       BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 52.440) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 54.351) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->pieces.size(), 1);
+       BOOST_CHECK_EQUAL (i->pieces.front().text, "We have emboldened this");
+       BOOST_CHECK_EQUAL (i->pieces.front().bold, true);
+
+       ++i;
+       BOOST_CHECK (i != s._subtitles.end ());
+       BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 54.440) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 56.590) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->pieces.size(), 1);
+       BOOST_CHECK_EQUAL (i->pieces.front().text, "And italicised this.");
+       BOOST_CHECK_EQUAL (i->pieces.front().italic, true);
+
+       ++i;
+       BOOST_CHECK (i != s._subtitles.end ());
+       BOOST_CHECK_EQUAL (i->from, ((1 * 60) + 56.680) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->to, ((1 * 60) + 58.955) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->pieces.size(), 1);
+       BOOST_CHECK_EQUAL (i->pieces.front().text, "Shall I compare thee to a summers' day?");
+
+       ++i;
+       BOOST_CHECK (i != s._subtitles.end ());
+       BOOST_CHECK_EQUAL (i->from, ((2 * 60) + 0.840) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->to, ((2 * 60) + 3.400) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->pieces.size(), 1);
+       BOOST_CHECK_EQUAL (i->pieces.front().text, "Is this a dagger I see before me?");
+
+       ++i;
+       BOOST_CHECK (i != s._subtitles.end ());
+       BOOST_CHECK_EQUAL (i->from, ((3 * 60) + 54.560) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->to, ((3 * 60) + 56.471) * TIME_HZ);
+       BOOST_CHECK_EQUAL (i->pieces.size(), 1);
+       BOOST_CHECK_EQUAL (i->pieces.front().text, "Hello world.");
+
+       ++i;
+       BOOST_CHECK (i == s._subtitles.end ());
+}
+
+/** Test rendering of a SubRip subtitle */
+BOOST_AUTO_TEST_CASE (subrip_render_test)
+{
+       shared_ptr<SubRipContent> content (new SubRipContent (shared_ptr<Film> (), "test/data/subrip.srt"));
+       content->examine (shared_ptr<Job> ());
+       BOOST_CHECK_EQUAL (content->full_length(), ((3 * 60) + 56.471) * TIME_HZ);
+
+       shared_ptr<Film> film = new_test_film ("subrip_render_test");
+
+       shared_ptr<SubRipDecoder> decoder (new SubRipDecoder (film, content));
+       shared_ptr<DecodedTextSubtitle> dts = dynamic_pointer_cast<DecodedTextSubtitle> (decoder->peek ());
+
+       shared_ptr<Image> image;
+       Position<int> position;
+       render_subtitles (dts->subs, libdcp::Size (1998, 1080), image, position);
+       write_image (image, "build/test/subrip_render_test.png");
+       check_file ("build/test/subrip_render_test.png", "test/data/subrip_render_test.png");
+}
index c0f732c46d22c9f558e6c897772d3833cf51e39f..f9d761da305dd150e858a48c794710e4615c49a5 100644 (file)
@@ -19,6 +19,7 @@
 
 #include <vector>
 #include <list>
+#include <Magick++.h>
 #include <libxml++/libxml++.h>
 #include <libdcp/dcp.h>
 #include "lib/config.h"
@@ -29,6 +30,7 @@
 #include "lib/job.h"
 #include "lib/cross.h"
 #include "lib/server_finder.h"
+#include "lib/image.h"
 #define BOOST_TEST_DYN_LINK
 #define BOOST_TEST_MODULE dcpomatic_test
 #include <boost/test/unit_test.hpp>
@@ -95,7 +97,7 @@ new_test_film (string name)
        return f;
 }
 
-static void
+void
 check_file (boost::filesystem::path ref, boost::filesystem::path check)
 {
        uintmax_t N = boost::filesystem::file_size (ref);
@@ -231,3 +233,12 @@ wait_for_jobs ()
 
        ui_signaller->ui_idle ();
 }
+
+void
+write_image (shared_ptr<const Image> image, boost::filesystem::path file)
+{
+       using namespace MagickCore;
+
+       Magick::Image m (image->size().width, image->size().height, "ARGB", CharPixel, (void *) image->data()[0]);
+       m.write (file.string ());
+}
index c582966655ddc6c6ed4b6f8e2ceb0cefb14f240f..b6347a5cad354f338b5c2a23ca67ccd8b46e4340 100644 (file)
 #include <boost/filesystem.hpp>
 
 class Film;
+class Image;
 
 extern void wait_for_jobs ();
 extern boost::shared_ptr<Film> new_test_film (std::string);
 extern void check_dcp (boost::filesystem::path, boost::filesystem::path);
+extern void check_file (boost::filesystem::path ref, boost::filesystem::path check);
 extern void check_xml (boost::filesystem::path, boost::filesystem::path, std::list<std::string>);
 extern boost::filesystem::path test_film_dir (std::string);
+extern void write_image (boost::shared_ptr<const Image> image, boost::filesystem::path file);
index a35a6cbf573b8605ea072ce19b10c20156c04472..de9e9f25a41a8c66f4e22783f59cf3092112a328 100644 (file)
@@ -44,6 +44,7 @@ def build(bld):
                  silence_padding_test.cc
                  skip_frame_test.cc
                  stream_test.cc
+                 subrip_test.cc
                  test.cc
                  threed_test.cc
                  util_test.cc
diff --git a/wscript b/wscript
index d722524590e75d137a484fff21071ee6c58f8c08..772db929369d1382d8b3c6fc294ca3080cd6aa4f 100644 (file)
--- a/wscript
+++ b/wscript
@@ -145,6 +145,8 @@ def configure(conf):
     conf.check_cfg(package='libxml++-2.6', args='--cflags --libs', uselib_store='XML++', mandatory=True)
     conf.check_cfg(package='libcurl', args='--cflags --libs', uselib_store='CURL', mandatory=True)
     conf.check_cfg(package='libzip', args='--cflags --libs', uselib_store='ZIP', mandatory=True)
+    conf.check_cfg(package='pangomm-1.4', args='--cflags --libs', uselib_store='PANGOMM', mandatory=True)
+    conf.check_cfg(package='cairomm-1.0', args='--cflags --libs', uselib_store='CAIROMM', mandatory=True)
 
     conf.check_cxx(fragment="""
                             #include <boost/version.hpp>\n