Basic and slightly inaccurate support for <Space> in subtitles (#2103).
[dcpomatic.git] / src / lib / render_text.cc
index 76c0fb79d0917ca0b81ae95d3113363381a44586..bcf04147a1af14496ed187e5824bbf867a70da93 100644 (file)
@@ -55,28 +55,81 @@ static FcConfig* fc_config = nullptr;
 static list<pair<boost::filesystem::path, string>> fc_config_fonts;
 
 
+/** Create a Pango layout using a dummy context which we can use to calculate the size
+ *  of the text we will render.  Then we can transfer the layout over to the real context
+ *  for the actual render.
+ */
+static Glib::RefPtr<Pango::Layout>
+create_layout()
+{
+       auto c_font_map = pango_cairo_font_map_new ();
+       DCPOMATIC_ASSERT (c_font_map);
+       auto font_map = Glib::wrap (c_font_map);
+       auto c_context = pango_font_map_create_context (c_font_map);
+       DCPOMATIC_ASSERT (c_context);
+       auto context = Glib::wrap (c_context);
+       return Pango::Layout::create (context);
+}
+
+
+static void
+setup_layout (Glib::RefPtr<Pango::Layout> layout, string font_name, string markup)
+{
+       layout->set_alignment (Pango::ALIGN_LEFT);
+       Pango::FontDescription font (font_name);
+       layout->set_font_description (font);
+       layout->set_markup (markup);
+}
+
+
 string
-marked_up (list<StringText> subtitles, int target_height, float fade_factor)
+marked_up (list<StringText> subtitles, int target_height, float fade_factor, string font_name)
 {
-       string out;
+       auto constexpr pixels_to_1024ths_point = 72 * 1024 / 96;
 
-       for (auto const& i: subtitles) {
-               out += "<span ";
-               if (i.italic()) {
-                       out += "style=\"italic\" ";
+       auto make_span = [target_height, fade_factor](StringText const& subtitle, string text, string extra_attribute) {
+               string span;
+               span += "<span ";
+               if (subtitle.italic()) {
+                       span += "style=\"italic\" ";
                }
-               if (i.bold()) {
-                       out += "weight=\"bold\" ";
+               if (subtitle.bold()) {
+                       span += "weight=\"bold\" ";
                }
-               if (i.underline()) {
-                       out += "underline=\"single\" ";
+               if (subtitle.underline()) {
+                       span += "underline=\"single\" ";
                }
-               out += "size=\"" + dcp::raw_convert<string>(i.size_in_pixels(target_height) * 72 * 1024 / 96) + "\" ";
+               span += "size=\"" + dcp::raw_convert<string>(subtitle.size_in_pixels(target_height) * pixels_to_1024ths_point) + "\" ";
                /* Between 1-65535 inclusive, apparently... */
-               out += "alpha=\"" + dcp::raw_convert<string>(int(floor(fade_factor * 65534)) + 1) + "\" ";
-               out += "color=\"#" + i.colour().to_rgb_string() + "\">";
-               out += i.text();
-               out += "</span>";
+               span += "alpha=\"" + dcp::raw_convert<string>(int(floor(fade_factor * 65534)) + 1) + "\" ";
+               span += "color=\"#" + subtitle.colour().to_rgb_string() + "\"";
+               if (!extra_attribute.empty()) {
+                       span += " " + extra_attribute;
+               }
+               span += ">";
+               span += text;
+               span += "</span>";
+               return span;
+       };
+
+       string out;
+       for (auto const& i: subtitles) {
+               if (std::abs(i.space_before()) > dcp::SPACE_BEFORE_EPSILON) {
+                       /* We need to insert some horizontal space into the layout.  The only way I can find to do this
+                        * is to write a " " with some special letter_spacing.  As far as I can see, such a space will
+                        * be written with letter_spacing either side.  This means that to get a horizontal space x we
+                        * need to write a " " with letter spacing (x - s) / 2, where s is the width of the " ".
+                        */
+                       auto layout = create_layout();
+                       setup_layout(layout, font_name, make_span(i, " ", {}));
+                       int space_width;
+                       int dummy;
+                       layout->get_pixel_size(space_width, dummy);
+                       auto spacing = ((i.space_before() * i.size_in_pixels(target_height) - space_width) / 2) * pixels_to_1024ths_point;
+                       out += make_span(i, " ", "letter_spacing=\"" + dcp::raw_convert<string>(spacing) + "\"");
+               }
+
+               out += make_span(i, i.text(), {});
        }
 
        return out;
@@ -93,7 +146,9 @@ set_source_rgba (Cairo::RefPtr<Cairo::Context> context, dcp::Colour colour, floa
 static shared_ptr<Image>
 create_image (dcp::Size size)
 {
-       /* FFmpeg BGRA means first byte blue, second byte green, third byte red, fourth byte alpha */
+       /* FFmpeg BGRA means first byte blue, second byte green, third byte red, fourth byte alpha.
+        * This must be COMPACT as we're using it with Cairo::ImageSurface::create
+        */
        auto image = make_shared<Image>(AV_PIX_FMT_BGRA, size, Image::Alignment::COMPACT);
        image->make_black ();
        return image;
@@ -103,6 +158,11 @@ create_image (dcp::Size size)
 static Cairo::RefPtr<Cairo::ImageSurface>
 create_surface (shared_ptr<Image> image)
 {
+       /* XXX: I don't think it's guaranteed that format_stride_for_width will return a stride without any padding,
+        * so it's lucky that this works.
+        */
+       DCPOMATIC_ASSERT (image->alignment() == Image::Alignment::COMPACT);
+       DCPOMATIC_ASSERT (image->pixel_format() == AV_PIX_FMT_BGRA);
        return Cairo::ImageSurface::create (
                image->data()[0],
                Cairo::FORMAT_ARGB32,
@@ -252,33 +312,6 @@ y_position (StringText const& first, int target_height, int layout_height)
 }
 
 
-static void
-setup_layout (Glib::RefPtr<Pango::Layout> layout, string font_name, string markup)
-{
-       layout->set_alignment (Pango::ALIGN_LEFT);
-       Pango::FontDescription font (font_name);
-       layout->set_font_description (font);
-       layout->set_markup (markup);
-}
-
-
-/** Create a Pango layout using a dummy context which we can use to calculate the size
- *  of the text we will render.  Then we can transfer the layout over to the real context
- *  for the actual render.
- */
-static Glib::RefPtr<Pango::Layout>
-create_layout()
-{
-       auto c_font_map = pango_cairo_font_map_new ();
-       DCPOMATIC_ASSERT (c_font_map);
-       auto font_map = Glib::wrap (c_font_map);
-       auto c_context = pango_font_map_create_context (c_font_map);
-       DCPOMATIC_ASSERT (c_context);
-       auto context = Glib::wrap (c_context);
-       return Pango::Layout::create (context);
-}
-
-
 /** @param subtitles A list of subtitles that are all on the same line,
  *  at the same time and with the same fade in/out.
  */
@@ -294,7 +327,7 @@ render_line (list<StringText> subtitles, list<shared_ptr<Font>> fonts, dcp::Size
 
        auto const font_name = setup_font (first, fonts);
        auto const fade_factor = calculate_fade_factor (first, time, frame_rate);
-       auto const markup = marked_up (subtitles, target.height, fade_factor);
+       auto const markup = marked_up (subtitles, target.height, fade_factor, font_name);
        auto layout = create_layout ();
        setup_layout (layout, font_name, markup);
        dcp::Size size;