Write one TTI block per subtitle line for binary STL.
[libsub.git] / src / stl_binary_writer.cc
1 /*
2     Copyright (C) 2014-2020 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 /** @file  src/stl_binary_writer.cc
21  *  @brief Writer for STL binary files.
22  */
23
24 #include "stl_binary_writer.h"
25 #include "subtitle.h"
26 #include "iso6937.h"
27 #include "stl_util.h"
28 #include "compose.hpp"
29 #include "sub_assert.h"
30 #include <boost/locale.hpp>
31 #include <boost/foreach.hpp>
32 #include <list>
33 #include <cmath>
34 #include <fstream>
35 #include <iomanip>
36 #include <set>
37
38 using std::list;
39 using std::set;
40 using std::ofstream;
41 using std::string;
42 using std::setw;
43 using std::setfill;
44 using std::max;
45 using std::cout;
46 using boost::locale::conv::utf_to_utf;
47 using boost::optional;
48 using namespace sub;
49
50 /** Arbitrary number which to divide the screen into rows; e.g.
51  *  64 here would mean that there are 64 addressable vertical positions
52  *  on the screen, each 1/64th of the screen height tall.
53  *
54  *  The magic 23 makes our output agree more closely with
55  *  AnnotationEdit, which makes life easier when testing.
56  */
57 static int const ROWS = 23;
58
59 static void
60 put_string (char* p, string s)
61 {
62         memcpy (p, s.c_str (), s.length ());
63 }
64
65 static void
66 put_string (char* p, unsigned int n, string s)
67 {
68         SUB_ASSERT (s.length() <= n);
69
70         memcpy (p, s.c_str (), s.length ());
71         memset (p + s.length(), ' ', n - s.length ());
72 }
73
74 static void
75 put_int_as_string (char* p, int v, unsigned int n)
76 {
77         locked_stringstream s;
78         /* Be careful to ensure we get no thousands separators */
79         s.imbue (std::locale::classic ());
80         s << setw (n) << setfill ('0');
81         s << v;
82         SUB_ASSERT (s.str().length() == n);
83         put_string (p, s.str ());
84 }
85
86 static void
87 put_int_as_int (char* p, int v, unsigned int n)
88 {
89         for (unsigned int i = 0; i < n; ++i) {
90                 *p++ = (v & ((1 << ((i + 1) * 8)) - 1)) >> (i * 8);
91         }
92 }
93
94 static int
95 vertical_position (sub::Line const & line)
96 {
97         int vp = 0;
98         if (line.vertical_position.proportional) {
99                 switch (line.vertical_position.reference.get_value_or (TOP_OF_SCREEN)) {
100                 case TOP_OF_SCREEN:
101                         vp = rint (line.vertical_position.proportional.get() * ROWS);
102                         break;
103                 case VERTICAL_CENTRE_OF_SCREEN:
104                         vp = rint (line.vertical_position.proportional.get() * ROWS + (ROWS / 2.0));
105                         break;
106                 case BOTTOM_OF_SCREEN:
107                         vp = rint (ROWS - (line.vertical_position.proportional.get() * ROWS));
108                         break;
109                 default:
110                         break;
111                 }
112         } else if (line.vertical_position.line) {
113                 float const prop = float (line.vertical_position.line.get()) / line.vertical_position.lines.get ();
114                 switch (line.vertical_position.reference.get_value_or (TOP_OF_SCREEN)) {
115                 case TOP_OF_SCREEN:
116                         vp = prop * ROWS;
117                         break;
118                 case VERTICAL_CENTRE_OF_SCREEN:
119                         vp = (prop + 0.5) * ROWS;
120                         break;
121                 case BOTTOM_OF_SCREEN:
122                         vp = (1 - prop) * ROWS;
123                         break;
124                 default:
125                         break;
126                 }
127         }
128
129         return vp;
130 }
131
132 /** @param language ISO 3-character country code for the language of the subtitles */
133 void
134 sub::write_stl_binary (
135         list<Subtitle> subtitles,
136         float frames_per_second,
137         Language language,
138         string original_programme_title,
139         string original_episode_title,
140         string translated_programme_title,
141         string translated_episode_title,
142         string translator_name,
143         string translator_contact_details,
144         string creation_date,
145         string revision_date,
146         int revision_number,
147         string country_of_origin,
148         string publisher,
149         string editor_name,
150         string editor_contact_details,
151         boost::filesystem::path file_name
152         )
153 {
154         SUB_ASSERT (original_programme_title.size() <= 32);
155         SUB_ASSERT (original_episode_title.size() <= 32);
156         SUB_ASSERT (translated_programme_title.size() <= 32);
157         SUB_ASSERT (translated_episode_title.size() <= 32);
158         SUB_ASSERT (translator_name.size() <= 32);
159         SUB_ASSERT (translator_contact_details.size() <= 32);
160         SUB_ASSERT (creation_date.size() == 6);
161         SUB_ASSERT (revision_date.size() == 6);
162         SUB_ASSERT (revision_number <= 99);
163         SUB_ASSERT (country_of_origin.size() == 3);
164         SUB_ASSERT (publisher.size() <= 32);
165         SUB_ASSERT (editor_name.size() <= 32);
166         SUB_ASSERT (editor_contact_details.size() <= 32);
167
168         char* buffer = new char[1024];
169         memset (buffer, 0, 1024);
170         ofstream output (file_name.string().c_str ());
171         STLBinaryTables tables;
172
173         /* Find the longest subtitle in characters and count the number of lines we have */
174         int longest = 0;
175         int lines = 0;
176         BOOST_FOREACH (Subtitle const& i, subtitles) {
177                 BOOST_FOREACH (Line const& j, i.lines) {
178                         int t = 0;
179                         BOOST_FOREACH (Block const& k, j.blocks) {
180                                 t += k.text.size ();
181                         }
182                         longest = std::max (longest, t);
183                         ++lines;
184                 }
185         }
186
187         /* Code page: 850 */
188         put_string (buffer + 0, "850");
189         /* Disk format code */
190         put_string (buffer + 3, stl_frame_rate_to_dfc (frames_per_second));
191         /* Display standard code: open subtitling */
192         put_string (buffer + 11, "0");
193         /* Character code table: Latin (ISO 6937) */
194         put_string (buffer + 12, "00");
195         put_string (buffer + 14, tables.language_enum_to_file (language));
196         put_string (buffer + 16, 32, original_programme_title);
197         put_string (buffer + 48, 32, original_episode_title);
198         put_string (buffer + 80, 32, translated_programme_title);
199         put_string (buffer + 112, 32, translated_episode_title);
200         put_string (buffer + 144, 32, translator_name);
201         put_string (buffer + 176, 32, translator_contact_details);
202         /* Subtitle list reference code */
203         put_string (buffer + 208, "0000000000000000");
204         put_string (buffer + 224, creation_date);
205         put_string (buffer + 230, revision_date);
206         put_int_as_string (buffer + 236, revision_number, 2);
207         /* TTI blocks */
208         put_int_as_string (buffer + 238, lines, 5);
209         /* Total number of subtitles */
210         put_int_as_string (buffer + 243, subtitles.size(), 5);
211         /* Total number of subtitle groups */
212         put_string (buffer + 248, "001");
213         /* Maximum number of displayable characters in any text row */
214         put_int_as_string (buffer + 251, longest, 2);
215         /* Maximum number of displayable rows */
216         put_int_as_string (buffer + 253, ROWS, 2);
217         /* Time code status */
218         put_string (buffer + 255, "1");
219         /* Start-of-programme time code */
220         put_string (buffer + 256, "00000000");
221         /* First-in-cue time code */
222         put_string (buffer + 264, "00000000");
223         /* Total number of disks */
224         put_string (buffer + 272, "1");
225         /* Disk sequence number */
226         put_string (buffer + 273, "1");
227         put_string (buffer + 274, 3, country_of_origin);
228         put_string (buffer + 277, 32, publisher);
229         put_string (buffer + 309, 32, editor_name);
230         put_string (buffer + 341, 32, editor_contact_details);
231
232         output.write (buffer, 1024);
233
234         BOOST_FOREACH (Subtitle const& i, subtitles) {
235
236                 size_t line = 0;
237                 BOOST_FOREACH (Line const& j, i.lines) {
238
239                         memset (buffer, 0, 128);
240
241                         /* XXX: these should increment, surely! */
242                         /* Subtitle group number */
243                         put_int_as_int (buffer + 0, 1, 1);
244                         /* Subtitle number */
245                         put_int_as_int (buffer + 1, 0, 2);
246                         /* Extension block number.  These are indexed from zero except the last one is 255 */
247                         put_int_as_int (buffer + 3, line == (i.lines.size() - 1) ? 255 : line, 1);
248                         /* Cumulative status */
249                         put_int_as_int (buffer + 4, tables.cumulative_status_enum_to_file (CUMULATIVE_STATUS_NOT_CUMULATIVE), 1);
250                         /* Time code in */
251                         put_int_as_int (buffer + 5, i.from.hours(), 1);
252                         put_int_as_int (buffer + 6, i.from.minutes(), 1);
253                         put_int_as_int (buffer + 7, i.from.seconds(), 1);
254                         put_int_as_int (buffer + 8, i.from.frames_at(sub::Rational(frames_per_second * 1000, 1000)), 1);
255                         /* Time code out */
256                         put_int_as_int (buffer + 9, i.to.hours(), 1);
257                         put_int_as_int (buffer + 10, i.to.minutes(), 1);
258                         put_int_as_int (buffer + 11, i.to.seconds(), 1);
259                         put_int_as_int (buffer + 12, i.to.frames_at(sub::Rational(frames_per_second * 1000, 1000)), 1);
260                         /* Vertical position */
261                         put_int_as_int (buffer + 13, vertical_position(j), 1);
262
263                         /* Justification code */
264                         switch (j.horizontal_position.reference) {
265                         case LEFT_OF_SCREEN:
266                                 put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_LEFT), 1);
267                                 break;
268                         case HORIZONTAL_CENTRE_OF_SCREEN:
269                                 put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_CENTRE), 1);
270                                 break;
271                         case RIGHT_OF_SCREEN:
272                                 put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_RIGHT), 1);
273                                 break;
274                         }
275
276                         /* Comment flag */
277                         put_int_as_int (buffer + 15, tables.comment_enum_to_file (COMMENT_NO), 1);
278
279                         /* Text */
280                         string text;
281                         bool italic = false;
282                         bool underline = false;
283
284                         BOOST_FOREACH (Block const& k, j.blocks) {
285                                 if (k.underline && !underline) {
286                                         text += "\x82";
287                                         underline = true;
288                                 } else if (underline && !k.underline) {
289                                         text += "\x83";
290                                         underline = false;
291                                 }
292                                 if (k.italic && !italic) {
293                                         text += "\x80";
294                                         italic = true;
295                                 } else if (italic && !k.italic) {
296                                         text += "\x81";
297                                         italic = false;
298                                 }
299
300                                 text += utf16_to_iso6937 (utf_to_utf<wchar_t> (k.text));
301                         }
302
303                         /* Turn italic/underline off before the end of this subtitle */
304
305                         if (underline) {
306                                 text += "\x83";
307                         }
308
309                         if (italic) {
310                                 text += "\x81";
311                         }
312
313                         if (text.length() > 111) {
314                                 text = text.substr (111);
315                         }
316
317                         while (text.length() < 112) {
318                                 text += "\x8F";
319                         }
320
321                         put_string (buffer + 16, text);
322                         output.write (buffer, 128);
323                         ++line;
324                 }
325         }
326
327         delete[] buffer;
328 }