/*
- Copyright (C) 2014 Carl Hetherington <cth@carlh.net>
+ Copyright (C) 2014-2020 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
*/
-#include <list>
-#include <cmath>
-#include <fstream>
+/** @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 <boost/algorithm/string.hpp>
+#include <cmath>
+#include <fstream>
+#include <iomanip>
+#include <set>
+#include <vector>
-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 std::vector;
+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)
{
}
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 ());
+}
+
+/** @param v Value
+ * @param n Width to zero-pad v to.
+ */
+static void
+put_int_as_string (char* p, int v, unsigned int n)
+{
+ char buffer[64];
+
+ switch (n) {
+ case 2:
+ snprintf (buffer, sizeof(buffer), "%02d", v);
+ break;
+ case 5:
+ snprintf (buffer, sizeof(buffer), "%05d", v);
+ break;
+ default:
+ SUB_ASSERT (false);
+ }
+
+ string s = buffer;
+
+ struct lconv* lc = localeconv ();
+ boost::algorithm::replace_all (s, lc->thousands_sep, "");
+ boost::algorithm::replace_all (s, lc->decimal_point, ".");
+
+ put_string (p, s);
+}
+
+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 VERTICAL_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 VERTICAL_CENTRE_OF_SCREEN:
+ vp = (prop + 0.5) * ROWS;
+ break;
+ case BOTTOM_OF_SCREEN:
+ vp = (1 - prop) * ROWS;
+ break;
+ default:
+ break;
+ }
+ }
+
+ return vp;
+}
+
+vector<char*>
+make_tti_blocks (vector<Subtitle> const& subtitles, STLBinaryTables const& tables, float frames_per_second)
+{
+ static int const tti_size = 128;
+ vector<char*> tti;
+
+ /* Buffer to build the TTI blocks in */
+ char buffer[tti_size];
+
+ for (auto const& i: subtitles) {
+
+ /* Find the top vertical position of this subtitle */
+ optional<int> top;
+ for (auto const& j: i.lines) {
+ int const vp = vertical_position (j);
+ if (!top || vp < top.get ()) {
+ top = vp;
+ }
+ }
+
+ /* Work out the text */
+ string text;
+ bool italic = false;
+ bool underline = false;
+ optional<int> last_vp;
+
+ for (auto const& j: i.lines) {
+
+ /* CR/LF down to this line */
+ int const vp = vertical_position (j);
+
+ if (last_vp) {
+ for (int k = last_vp.get(); k < vp; ++k) {
+ text += "\x8A";
+ }
+ }
+
+ last_vp = vp;
+
+ for (auto const& k: j.blocks) {
+ 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";
+ }
+
+ /* Make sure there's at least one end-of-line */
+ text += "\x8F";
+
+ /* Now write this text in 112 byte chunks (TTI blocks). Only the first TTI
+ block's cumulative status, timecodes, vertical position, justification code
+ and comment flag are taken into account by the reader.
+ */
+
+ /* Set up the first part of the block */
+
+ /* XXX: these should increment, surely! */
+ /* Subtitle group number */
+ put_int_as_int (buffer + 0, 1, 1);
+ /* Subtitle number */
+ put_int_as_int (buffer + 1, 0, 2);
+ /* 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.reference) {
+ case LEFT_OF_SCREEN:
+ put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_LEFT), 1);
+ break;
+ case HORIZONTAL_CENTRE_OF_SCREEN:
+ put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_CENTRE), 1);
+ break;
+ case RIGHT_OF_SCREEN:
+ 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);
+
+ /* Now make as many blocks as are needed to add all the text */
+ size_t const block_size = 112;
+ size_t offset = 0;
+ int block_number = 0;
+ while (offset < text.length()) {
+ size_t this_time = std::min(block_size, text.length() - offset);
+ put_string (buffer + 16, text.substr(offset, this_time) + string(block_size - this_time, '\x8f'));
+ offset += this_time;
+
+ /* Extension block number. Count up from 0 but use 0xff for the last one */
+ put_int_as_int (buffer + 3, offset == text.length() ? 0xff : block_number, 1);
+ ++block_number;
+
+ char* finished = new char[tti_size];
+ memcpy (finished, buffer, tti_size);
+ tti.push_back (finished);
+ }
+ }
+
+ return tti;
}
+
+
/** @param language ISO 3-character country code for the language of the subtitles */
void
sub::write_stl_binary (
- list<Subtitle> subtitles,
- float frames_per_second,
- string language,
- string original_programme_title,
- string original_episode_title,
- string translated_programme_title,
- string translated_episode_title,
- string translator_name,
- string translator_contact_details,
- string creation_date,
- string revision_date,
- int revision_number,
- string country_of_origin,
- string publisher,
- string editor_name,
- string editor_contact_details,
- boost::filesystem::path file_name
- )
+ vector<Subtitle> subtitles,
+ float frames_per_second,
+ Language language,
+ string original_programme_title,
+ string original_episode_title,
+ string translated_programme_title,
+ string translated_episode_title,
+ string translator_name,
+ string translator_contact_details,
+ string creation_date,
+ string revision_date,
+ int revision_number,
+ string country_of_origin,
+ string publisher,
+ string editor_name,
+ string editor_contact_details,
+ 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);
-
- char* buffer = new char[1024];
- ofstream output (file_name.string().c_str ());
-
+ 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[1024];
+ memset (buffer, 0, 1024);
+ STLBinaryTables tables;
+
+ /* Find the longest subtitle in characters */
+
+ int longest = 0;
+
+ for (auto const& i: subtitles) {
+ for (auto const& j: i.lines) {
+ int t = 0;
+ for (auto const& k: j.blocks) {
+ t += k.text.size ();
+ }
+ longest = std::max (longest, t);
+ }
+ }
+
+ vector<char*> tti_blocks = make_tti_blocks (subtitles, tables, frames_per_second);
+
/* 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);
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, tti_blocks.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 */
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);
+ ofstream output (file_name.string().c_str());
output.write (buffer, 1024);
-
- delete[] buffer;
+ for (auto i: tti_blocks) {
+ output.write (i, 128);
+ delete[] i;
+ }
}