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