Replace list with vector in most of the API.
[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/algorithm/string.hpp>
32 #include <boost/foreach.hpp>
33 #include <cmath>
34 #include <fstream>
35 #include <iomanip>
36 #include <set>
37 #include <vector>
38
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 std::vector;
47 using boost::locale::conv::utf_to_utf;
48 using boost::optional;
49 using namespace sub;
50
51 /** Arbitrary number which to divide the screen into rows; e.g.
52  *  64 here would mean that there are 64 addressable vertical positions
53  *  on the screen, each 1/64th of the screen height tall.
54  *
55  *  The magic 23 makes our output agree more closely with
56  *  AnnotationEdit, which makes life easier when testing.
57  */
58 static int const ROWS = 23;
59
60 static void
61 put_string (char* p, string s)
62 {
63         memcpy (p, s.c_str (), s.length ());
64 }
65
66 static void
67 put_string (char* p, unsigned int n, string s)
68 {
69         SUB_ASSERT (s.length() <= n);
70
71         memcpy (p, s.c_str (), s.length ());
72         memset (p + s.length(), ' ', n - s.length ());
73 }
74
75 /** @param v Value
76  *  @param n Width to zero-pad v to.
77  */
78 static void
79 put_int_as_string (char* p, int v, unsigned int n)
80 {
81         char buffer[64];
82
83         switch (n) {
84         case 2:
85                 snprintf (buffer, sizeof(buffer), "%02d", v);
86                 break;
87         case 5:
88                 snprintf (buffer, sizeof(buffer), "%05d", v);
89                 break;
90         default:
91                 SUB_ASSERT (false);
92         }
93
94         string s = buffer;
95
96         struct lconv* lc = localeconv ();
97         boost::algorithm::replace_all (s, lc->thousands_sep, "");
98         boost::algorithm::replace_all (s, lc->decimal_point, ".");
99
100         put_string (p, s);
101 }
102
103 static void
104 put_int_as_int (char* p, int v, unsigned int n)
105 {
106         for (unsigned int i = 0; i < n; ++i) {
107                 *p++ = (v & ((1 << ((i + 1) * 8)) - 1)) >> (i * 8);
108         }
109 }
110
111 static int
112 vertical_position (sub::Line const & line)
113 {
114         int vp = 0;
115         if (line.vertical_position.proportional) {
116                 switch (line.vertical_position.reference.get_value_or (TOP_OF_SCREEN)) {
117                 case TOP_OF_SCREEN:
118                         vp = rint (line.vertical_position.proportional.get() * ROWS);
119                         break;
120                 case VERTICAL_CENTRE_OF_SCREEN:
121                         vp = rint (line.vertical_position.proportional.get() * ROWS + (ROWS / 2.0));
122                         break;
123                 case BOTTOM_OF_SCREEN:
124                         vp = rint (ROWS - (line.vertical_position.proportional.get() * ROWS));
125                         break;
126                 default:
127                         break;
128                 }
129         } else if (line.vertical_position.line) {
130                 float const prop = float (line.vertical_position.line.get()) / line.vertical_position.lines.get ();
131                 switch (line.vertical_position.reference.get_value_or (TOP_OF_SCREEN)) {
132                 case TOP_OF_SCREEN:
133                         vp = prop * ROWS;
134                         break;
135                 case VERTICAL_CENTRE_OF_SCREEN:
136                         vp = (prop + 0.5) * ROWS;
137                         break;
138                 case BOTTOM_OF_SCREEN:
139                         vp = (1 - prop) * ROWS;
140                         break;
141                 default:
142                         break;
143                 }
144         }
145
146         return vp;
147 }
148
149 vector<char*>
150 make_tti_blocks (vector<Subtitle> const& subtitles, STLBinaryTables const& tables, float frames_per_second)
151 {
152         static int const tti_size = 128;
153         vector<char*> tti;
154
155         /* Buffer to build the TTI blocks in */
156         char buffer[tti_size];
157
158         BOOST_FOREACH (Subtitle const& i, subtitles) {
159
160                 /* Find the top vertical position of this subtitle */
161                 optional<int> top;
162                 BOOST_FOREACH (Line const& j, i.lines) {
163                         int const vp = vertical_position (j);
164                         if (!top || vp < top.get ()) {
165                                 top = vp;
166                         }
167                 }
168
169                 /* Work out the text */
170                 string text;
171                 bool italic = false;
172                 bool underline = false;
173                 optional<int> last_vp;
174
175                 BOOST_FOREACH (Line const& j, i.lines) {
176
177                         /* CR/LF down to this line */
178                         int const vp = vertical_position (j);
179
180                         if (last_vp) {
181                                 for (int k = last_vp.get(); k < vp; ++k) {
182                                         text += "\x8A";
183                                 }
184                         }
185
186                         last_vp = vp;
187
188                         BOOST_FOREACH (Block const& k, j.blocks) {
189                                 if (k.underline && !underline) {
190                                         text += "\x82";
191                                         underline = true;
192                                 } else if (underline && !k.underline) {
193                                         text += "\x83";
194                                         underline = false;
195                                 }
196                                 if (k.italic && !italic) {
197                                         text += "\x80";
198                                         italic = true;
199                                 } else if (italic && !k.italic) {
200                                         text += "\x81";
201                                         italic = false;
202                                 }
203
204                                 text += utf16_to_iso6937 (utf_to_utf<wchar_t> (k.text));
205                         }
206                 }
207
208                 /* Turn italic/underline off before the end of this subtitle */
209                 if (underline) {
210                         text += "\x83";
211                 }
212                 if (italic) {
213                         text += "\x81";
214                 }
215
216                 /* Make sure there's at least one end-of-line */
217                 text += "\x8F";
218
219                 /* Now write this text in 112 byte chunks (TTI blocks).  Only the first TTI
220                    block's cumulative status, timecodes, vertical position, justification code
221                    and comment flag are taken into account by the reader.
222                    */
223
224                 /* Set up the first part of the block */
225
226                 /* XXX: these should increment, surely! */
227                 /* Subtitle group number */
228                 put_int_as_int (buffer + 0, 1, 1);
229                 /* Subtitle number */
230                 put_int_as_int (buffer + 1, 0, 2);
231                 /* Cumulative status */
232                 put_int_as_int (buffer + 4, tables.cumulative_status_enum_to_file (CUMULATIVE_STATUS_NOT_CUMULATIVE), 1);
233                 /* Time code in */
234                 put_int_as_int (buffer + 5, i.from.hours(), 1);
235                 put_int_as_int (buffer + 6, i.from.minutes(), 1);
236                 put_int_as_int (buffer + 7, i.from.seconds(), 1);
237                 put_int_as_int (buffer + 8, i.from.frames_at(sub::Rational(frames_per_second * 1000, 1000)), 1);
238                 /* Time code out */
239                 put_int_as_int (buffer + 9, i.to.hours(), 1);
240                 put_int_as_int (buffer + 10, i.to.minutes(), 1);
241                 put_int_as_int (buffer + 11, i.to.seconds(), 1);
242                 put_int_as_int (buffer + 12, i.to.frames_at(sub::Rational(frames_per_second * 1000, 1000)), 1);
243                 /* Vertical position */
244                 put_int_as_int (buffer + 13, top.get(), 1);
245
246                 /* Justification code */
247                 /* XXX: this assumes the first line has the right value */
248                 switch (i.lines.front().horizontal_position.reference) {
249                         case LEFT_OF_SCREEN:
250                                 put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_LEFT), 1);
251                                 break;
252                         case HORIZONTAL_CENTRE_OF_SCREEN:
253                                 put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_CENTRE), 1);
254                                 break;
255                         case RIGHT_OF_SCREEN:
256                                 put_int_as_int (buffer + 14, tables.justification_enum_to_file (JUSTIFICATION_RIGHT), 1);
257                                 break;
258                 }
259
260                 /* Comment flag */
261                 put_int_as_int (buffer + 15, tables.comment_enum_to_file (COMMENT_NO), 1);
262
263                 /* Now make as many blocks as are needed to add all the text */
264                 size_t const block_size = 112;
265                 size_t offset = 0;
266                 int block_number = 0;
267                 while (offset < text.length()) {
268                         size_t this_time = std::min(block_size, text.length() - offset);
269                         put_string (buffer + 16, text.substr(offset, this_time) + string(block_size - this_time, '\x8f'));
270                         offset += this_time;
271
272                         /* Extension block number.  Count up from 0 but use 0xff for the last one */
273                         put_int_as_int (buffer + 3, offset == text.length() ? 0xff : block_number, 1);
274                         ++block_number;
275
276                         char* finished = new char[tti_size];
277                         memcpy (finished, buffer, tti_size);
278                         tti.push_back (finished);
279                 }
280         }
281
282         return tti;
283 }
284
285
286
287 /** @param language ISO 3-character country code for the language of the subtitles */
288 void
289 sub::write_stl_binary (
290                 vector<Subtitle> subtitles,
291                 float frames_per_second,
292                 Language language,
293                 string original_programme_title,
294                 string original_episode_title,
295                 string translated_programme_title,
296                 string translated_episode_title,
297                 string translator_name,
298                 string translator_contact_details,
299                 string creation_date,
300                 string revision_date,
301                 int revision_number,
302                 string country_of_origin,
303                 string publisher,
304                 string editor_name,
305                 string editor_contact_details,
306                 boost::filesystem::path file_name
307                 )
308 {
309         SUB_ASSERT (original_programme_title.size() <= 32);
310         SUB_ASSERT (original_episode_title.size() <= 32);
311         SUB_ASSERT (translated_programme_title.size() <= 32);
312         SUB_ASSERT (translated_episode_title.size() <= 32);
313         SUB_ASSERT (translator_name.size() <= 32);
314         SUB_ASSERT (translator_contact_details.size() <= 32);
315         SUB_ASSERT (creation_date.size() == 6);
316         SUB_ASSERT (revision_date.size() == 6);
317         SUB_ASSERT (revision_number <= 99);
318         SUB_ASSERT (country_of_origin.size() == 3);
319         SUB_ASSERT (publisher.size() <= 32);
320         SUB_ASSERT (editor_name.size() <= 32);
321         SUB_ASSERT (editor_contact_details.size() <= 32);
322
323         char buffer[1024];
324         memset (buffer, 0, 1024);
325         STLBinaryTables tables;
326
327         /* Find the longest subtitle in characters */
328
329         int longest = 0;
330
331         BOOST_FOREACH (Subtitle const& i, subtitles) {
332                 BOOST_FOREACH (Line const& j, i.lines) {
333                         int t = 0;
334                         BOOST_FOREACH (Block const& k, j.blocks) {
335                                 t += k.text.size ();
336                         }
337                         longest = std::max (longest, t);
338                 }
339         }
340
341         vector<char*> tti_blocks = make_tti_blocks (subtitles, tables, frames_per_second);
342
343         /* Code page: 850 */
344         put_string (buffer + 0, "850");
345         /* Disk format code */
346         put_string (buffer + 3, stl_frame_rate_to_dfc (frames_per_second));
347         /* Display standard code: open subtitling */
348         put_string (buffer + 11, "0");
349         /* Character code table: Latin (ISO 6937) */
350         put_string (buffer + 12, "00");
351         put_string (buffer + 14, tables.language_enum_to_file (language));
352         put_string (buffer + 16, 32, original_programme_title);
353         put_string (buffer + 48, 32, original_episode_title);
354         put_string (buffer + 80, 32, translated_programme_title);
355         put_string (buffer + 112, 32, translated_episode_title);
356         put_string (buffer + 144, 32, translator_name);
357         put_string (buffer + 176, 32, translator_contact_details);
358         /* Subtitle list reference code */
359         put_string (buffer + 208, "0000000000000000");
360         put_string (buffer + 224, creation_date);
361         put_string (buffer + 230, revision_date);
362         put_int_as_string (buffer + 236, revision_number, 2);
363         /* TTI blocks */
364         put_int_as_string (buffer + 238, tti_blocks.size(), 5);
365         /* Total number of subtitles */
366         put_int_as_string (buffer + 243, subtitles.size(), 5);
367         /* Total number of subtitle groups */
368         put_string (buffer + 248, "001");
369         /* Maximum number of displayable characters in any text row */
370         put_int_as_string (buffer + 251, longest, 2);
371         /* Maximum number of displayable rows */
372         put_int_as_string (buffer + 253, ROWS, 2);
373         /* Time code status */
374         put_string (buffer + 255, "1");
375         /* Start-of-programme time code */
376         put_string (buffer + 256, "00000000");
377         /* First-in-cue time code */
378         put_string (buffer + 264, "00000000");
379         /* Total number of disks */
380         put_string (buffer + 272, "1");
381         /* Disk sequence number */
382         put_string (buffer + 273, "1");
383         put_string (buffer + 274, 3, country_of_origin);
384         put_string (buffer + 277, 32, publisher);
385         put_string (buffer + 309, 32, editor_name);
386         put_string (buffer + 341, 32, editor_contact_details);
387
388         ofstream output (file_name.string().c_str());
389         output.write (buffer, 1024);
390         BOOST_FOREACH (char* i, tti_blocks) {
391                 output.write (i, 128);
392                 delete[] i;
393         }
394 }