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