Fix failure to parse subrip where there are extra spaces in the time/position line.
[libsub.git] / src / stl_binary_writer.cc
index 34e4d73c1ecef2922ab1d43b5692b35fa7cde8fd..7ae75094d9bc19cc89171046265d20796b57d631 100644 (file)
 
 */
 
+/** @file  src/stl_binary_writer.cc
+ *  @brief Writer for STL binary files.
+ */
+
+#include "stl_binary_writer.h"
+#include "subtitle.h"
+#include "iso6937.h"
+#include "stl_util.h"
+#include "compose.hpp"
+#include "sub_assert.h"
+#include <boost/locale.hpp>
 #include <list>
 #include <cmath>
 #include <fstream>
-#include "stl_binary_writer.h"
-#include "compose.hpp"
+#include <iomanip>
+#include <set>
 
 using std::list;
+using std::set;
 using std::ofstream;
 using std::string;
+using std::setw;
+using std::setfill;
+using std::max;
+using std::cout;
+using boost::locale::conv::utf_to_utf;
+using boost::optional;
 using namespace sub;
 
+/** Arbitrary number which to divide the screen into rows; e.g.
+ *  64 here would mean that there are 64 addressable vertical positions
+ *  on the screen, each 1/64th of the screen height tall.
+ *
+ *  The magic 23 makes our output agree more closely with
+ *  AnnotationEdit, which makes life easier when testing.
+ */
+static int const ROWS = 23;
+
 static void
 put_string (char* p, string s)
 {
@@ -35,10 +62,70 @@ put_string (char* p, string s)
 }
 
 static void
-put_string (char* p, int n, string s)
+put_string (char* p, unsigned int n, string s)
 {
+       SUB_ASSERT (s.length() <= n);
+
        memcpy (p, s.c_str (), s.length ());
-       memset (p + s.length(), ' ', s.length () - n);
+       memset (p + s.length(), ' ', n - s.length ());
+}
+
+static void
+put_int_as_string (char* p, int v, unsigned int n)
+{
+       locked_stringstream s;
+       /* Be careful to ensure we get no thousands separators */
+       s.imbue (std::locale::classic ());
+       s << setw (n) << setfill ('0');
+       s << v;
+       SUB_ASSERT (s.str().length() == n);
+       put_string (p, s.str ());
+}
+
+static void
+put_int_as_int (char* p, int v, unsigned int n)
+{
+       for (unsigned int i = 0; i < n; ++i) {
+               *p++ = (v & ((1 << ((i + 1) * 8)) - 1)) >> (i * 8);
+       }
+}
+
+static int
+vertical_position (sub::Line const & line)
+{
+       int vp = 0;
+       if (line.vertical_position.proportional) {
+               switch (line.vertical_position.reference.get_value_or (TOP_OF_SCREEN)) {
+               case TOP_OF_SCREEN:
+                       vp = rint (line.vertical_position.proportional.get() * ROWS);
+                       break;
+               case CENTRE_OF_SCREEN:
+                       vp = rint (line.vertical_position.proportional.get() * ROWS + (ROWS / 2.0));
+                       break;
+               case BOTTOM_OF_SCREEN:
+                       vp = rint (ROWS - (line.vertical_position.proportional.get() * ROWS));
+                       break;
+               default:
+                       break;
+               }
+       } else if (line.vertical_position.line) {
+               float const prop = float (line.vertical_position.line.get()) / line.vertical_position.lines.get ();
+               switch (line.vertical_position.reference.get_value_or (TOP_OF_SCREEN)) {
+               case TOP_OF_SCREEN:
+                       vp = prop * ROWS;
+                       break;
+               case CENTRE_OF_SCREEN:
+                       vp = (prop + 0.5) * ROWS;
+                       break;
+               case BOTTOM_OF_SCREEN:
+                       vp = (1 - prop) * ROWS;
+                       break;
+               default:
+                       break;
+               }
+       }
+
+       return vp;
 }
 
 /** @param language ISO 3-character country code for the language of the subtitles */
@@ -46,7 +133,7 @@ void
 sub::write_stl_binary (
        list<Subtitle> subtitles,
        float frames_per_second,
-       string language,
+       Language language,
        string original_programme_title,
        string original_episode_title,
        string translated_programme_title,
@@ -63,33 +150,48 @@ sub::write_stl_binary (
        boost::filesystem::path file_name
        )
 {
-       assert (language.size() == 3);
-       assert (original_programme_title.size() <= 32);
-       assert (original_episode_title.size() <= 32);
-       assert (translated_programme_title.size() <= 32);
-       assert (translated_episode_title.size() <= 32);
-       assert (translator_name.size() <= 32);
-       assert (translator_contact_details.size() <= 32);
-       assert (creation_date.size() == 6);
-       assert (revision_date.size() == 6);
-       assert (revision_number <= 99);
-       assert (country_of_origin.size() == 3);
-       assert (publisher.size() <= 32);
-       assert (editor_name.size() <= 32);
-       assert (editor_contact_details.size() <= 32);
-       
+       SUB_ASSERT (original_programme_title.size() <= 32);
+       SUB_ASSERT (original_episode_title.size() <= 32);
+       SUB_ASSERT (translated_programme_title.size() <= 32);
+       SUB_ASSERT (translated_episode_title.size() <= 32);
+       SUB_ASSERT (translator_name.size() <= 32);
+       SUB_ASSERT (translator_contact_details.size() <= 32);
+       SUB_ASSERT (creation_date.size() == 6);
+       SUB_ASSERT (revision_date.size() == 6);
+       SUB_ASSERT (revision_number <= 99);
+       SUB_ASSERT (country_of_origin.size() == 3);
+       SUB_ASSERT (publisher.size() <= 32);
+       SUB_ASSERT (editor_name.size() <= 32);
+       SUB_ASSERT (editor_contact_details.size() <= 32);
+
        char* buffer = new char[1024];
+       memset (buffer, 0, 1024);
        ofstream output (file_name.string().c_str ());
-       
+       STLBinaryTables tables;
+
+       /* Find the longest subtitle in characters */
+
+       int longest = 0;
+
+       for (list<Subtitle>::const_iterator i = subtitles.begin(); i != subtitles.end(); ++i) {
+               for (list<Line>::const_iterator j = i->lines.begin(); j != i->lines.end(); ++j) {
+                       int t = 0;
+                       for (list<Block>::const_iterator k = j->blocks.begin(); k != j->blocks.end(); ++k) {
+                               t += k->text.size ();
+                       }
+                       longest = max (longest, t);
+               }
+       }
+
        /* Code page: 850 */
        put_string (buffer + 0, "850");
        /* Disk format code */
-       put_string (buffer + 3, String::compose ("STL%1.01", rint (frames_per_second)));
+       put_string (buffer + 3, stl_frame_rate_to_dfc (frames_per_second));
        /* Display standard code: open subtitling */
        put_string (buffer + 11, "0");
        /* Character code table: Latin (ISO 6937) */
        put_string (buffer + 12, "00");
-       put_string (buffer + 14, language);
+       put_string (buffer + 14, tables.language_enum_to_file (language));
        put_string (buffer + 16, 32, original_programme_title);
        put_string (buffer + 48, 32, original_episode_title);
        put_string (buffer + 80, 32, translated_programme_title);
@@ -100,19 +202,17 @@ sub::write_stl_binary (
        put_string (buffer + 208, "0000000000000000");
        put_string (buffer + 224, creation_date);
        put_string (buffer + 230, revision_date);
-       put_string (buffer + 236, String::compose ("%02d", revision_number));
+       put_int_as_string (buffer + 236, revision_number, 2);
        /* TTI blocks */
-       put_string (buffer + 238, String::compose ("%05d", subtitles.size ()));
+       put_int_as_string (buffer + 238, subtitles.size(), 5);
        /* Total number of subtitles */
-       put_string (buffer + 243, String::compose ("%05d", subtitles.size ()));
+       put_int_as_string (buffer + 243, subtitles.size(), 5);
        /* Total number of subtitle groups */
-       put_string (buffer + 248, "000");
+       put_string (buffer + 248, "001");
        /* Maximum number of displayable characters in any text row */
-       /* XXX */
-       put_string (buffer + 251, "99");
+       put_int_as_string (buffer + 251, longest, 2);
        /* Maximum number of displayable rows */
-       /* XXX */
-       put_string (buffer + 253, "99");
+       put_int_as_string (buffer + 253, ROWS, 2);
        /* Time code status */
        put_string (buffer + 255, "1");
        /* Start-of-programme time code */
@@ -123,12 +223,126 @@ sub::write_stl_binary (
        put_string (buffer + 272, "1");
        /* Disk sequence number */
        put_string (buffer + 273, "1");
-       put_string (buffer + 274, country_of_origin);
-       put_string (buffer + 277, publisher);
-       put_string (buffer + 309, editor_name);
-       put_string (buffer + 341, editor_contact_details);
+       put_string (buffer + 274, 3, country_of_origin);
+       put_string (buffer + 277, 32, publisher);
+       put_string (buffer + 309, 32, editor_name);
+       put_string (buffer + 341, 32, editor_contact_details);
 
        output.write (buffer, 1024);
 
+       for (list<Subtitle>::const_iterator i = subtitles.begin(); i != subtitles.end(); ++i) {
+
+               /* Find the top vertical position of this subtitle */
+               optional<int> top;
+               for (list<Line>::const_iterator j = i->lines.begin(); j != i->lines.end(); ++j) {
+                       int const vp = vertical_position (*j);
+                       if (!top || vp < top.get ()) {
+                               top = vp;
+                       }
+               }
+
+               memset (buffer, 0, 1024);
+
+               /* Subtitle group number */
+               put_int_as_int (buffer + 0, 1, 1);
+               /* Subtitle number */
+               put_int_as_int (buffer + 1, 0, 2);
+               /* Extension block number.  Use 0xff here to indicate that it is the last TTI
+                  block in this subtitle "set", as we only ever use one.
+               */
+               put_int_as_int (buffer + 3, 255, 1);
+               /* Cumulative status */
+               put_int_as_int (buffer + 4, tables.cumulative_status_enum_to_file (CUMULATIVE_STATUS_NOT_CUMULATIVE), 1);
+               /* Time code in */
+               put_int_as_int (buffer + 5, i->from.hours(), 1);
+               put_int_as_int (buffer + 6, i->from.minutes(), 1);
+               put_int_as_int (buffer + 7, i->from.seconds(), 1);
+               put_int_as_int (buffer + 8, i->from.frames_at(sub::Rational (frames_per_second * 1000, 1000)), 1);
+               /* Time code out */
+               put_int_as_int (buffer + 9, i->to.hours(), 1);
+               put_int_as_int (buffer + 10, i->to.minutes(), 1);
+               put_int_as_int (buffer + 11, i->to.seconds(), 1);
+               put_int_as_int (buffer + 12, i->to.frames_at(sub::Rational (frames_per_second * 1000, 1000)), 1);
+               /* Vertical position */
+               put_int_as_int (buffer + 13, top.get(), 1);
+
+               /* Justification code */
+               /* XXX: this assumes the first line has the right value */
+               switch (i->lines.front().horizontal_position) {
+               case LEFT:
+                       put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_LEFT), 1);
+                       break;
+               case CENTRE:
+                       put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_CENTRE), 1);
+                       break;
+               case RIGHT:
+                       put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_RIGHT), 1);
+                       break;
+               }
+
+               /* Comment flag */
+               put_int_as_int (buffer + 15, tables.comment_enum_to_file (COMMENT_NO), 1);
+
+               /* Text */
+               string text;
+               bool italic = false;
+               bool underline = false;
+               optional<int> last_vp;
+
+               for (list<Line>::const_iterator j = i->lines.begin(); j != i->lines.end(); ++j) {
+
+                       /* CR/LF down to this line */
+                       int const vp = vertical_position (*j);
+
+                       if (last_vp) {
+                               for (int i = last_vp.get(); i < vp; ++i) {
+                                       text += "\x8A";
+                               }
+                       }
+
+                       last_vp = vp;
+
+                       for (list<Block>::const_iterator k = j->blocks.begin(); k != j->blocks.end(); ++k) {
+                               if (k->underline && !underline) {
+                                       text += "\x82";
+                                       underline = true;
+                               } else if (underline && !k->underline) {
+                                       text += "\x83";
+                                       underline = false;
+                               }
+                               if (k->italic && !italic) {
+                                       text += "\x80";
+                                       italic = true;
+                               } else if (italic && !k->italic) {
+                                       text += "\x81";
+                                       italic = false;
+                               }
+
+                               text += utf16_to_iso6937 (utf_to_utf<wchar_t> (k->text));
+                       }
+               }
+
+               /* Turn italic/underline off before the end of this subtitle */
+
+               if (underline) {
+                       text += "\x83";
+               }
+
+               if (italic) {
+                       text += "\x81";
+               }
+
+               if (text.length() > 111) {
+                       text = text.substr (111);
+               }
+
+               while (text.length() < 112) {
+                       text += "\x8F";
+               }
+
+               put_string (buffer + 16, text);
+               output.write (buffer, 128);
+       }
+
        delete[] buffer;
 }