1 // Copyright (c) 2011 Niels Martin Hansen <nielsm@aegisub.org>
2 //
3 // Permission to use, copy, modify, and distribute this software for any
4 // purpose with or without fee is hereby granted, provided that the above
5 // copyright notice and this permission notice appear in all copies.
6 //
7 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 //
15 // Aegisub Project http://www.aegisub.org/
16 
17 /// @file subtitle_format_ebu3264.cpp
18 /// @see subtitle_format_ebu3264.h
19 /// @ingroup subtitle_io
20 
21 // This implements support for the EBU tech 3264 (1991) subtitling data exchange format.
22 // Work on support for this format was sponsored by Bandai.
23 
24 #include "subtitle_format_ebu3264.h"
25 
26 #include "ass_dialogue.h"
27 #include "ass_file.h"
28 #include "ass_style.h"
29 #include "compat.h"
30 #include "dialog_export_ebu3264.h"
31 #include "format.h"
32 #include "options.h"
33 #include "text_file_writer.h"
34 
35 #include <libaegisub/charset_conv.h>
36 #include <libaegisub/exception.h>
37 #include <libaegisub/io.h>
38 #include <libaegisub/line_wrap.h>
39 
40 #include <boost/algorithm/string/replace.hpp>
41 #include <wx/utils.h>
42 
43 namespace
44 {
45 #pragma pack(push, 1)
46 	/// General Subtitle Information block as it appears in the file
47 	struct BlockGSI
48 	{
49 		char cpn[3];    ///< code page number
50 		char dfc[8];    ///< disk format code
51 		char dsc;       ///< display standard code
52 		char cct[2];    ///< character code table number
53 		char lc[2];     ///< language code
54 		char opt[32];   ///< original programme title
55 		char oet[32];   ///< original episode title
56 		char tpt[32];   ///< translated programme title
57 		char tet[32];   ///< translated episode title
58 		char tn[32];    ///< translator name
59 		char tcd[32];   ///< translator contact details
60 		char slr[16];   ///< subtitle list reference code
61 		char cd[6];     ///< creation date
62 		char rd[6];     ///< revision date
63 		char rn[2];     ///< revision number
64 		char tnb[5];    ///< total number of TTI blocks
65 		char tns[5];    ///< total number of subtitles
66 		char tng[3];    ///< total number of subtitle groups
67 		char mnc[2];    ///< maximum number of displayable characters in a row
68 		char mnr[2];    ///< maximum number of displayable rows
69 		char tcs;       ///< time code: status
70 		char tcp[8];    ///< time code: start of programme
71 		char tcf[8];    ///< time code: first in-cue
72 		char tnd;       ///< total number of disks
73 		char dsn;       ///< disk sequence number
74 		char co[3];     ///< country of origin
75 		char pub[32];   ///< publisher
76 		char en[32];    ///< editor's name
77 		char ecd[32];   ///< editor's contact details
78 		char unused[75];
79 		char uda[576];  ///< user defined area
80 	};
81 
82 	/// Text and Timing Information block as it appears in the file
83 	struct BlockTTI
84 	{
85 		uint8_t     sgn; ///< subtitle group number
86 		uint16_t    sn;  ///< subtitle number
87 		uint8_t     ebn; ///< extension block number
88 		uint8_t     cs;  ///< cumulative status
89 		EbuTimecode tci; ///< time code in
90 		EbuTimecode tco; ///< time code out
91 		uint8_t     vp;  ///< vertical position
92 		uint8_t     jc;  ///< justification code
93 		uint8_t     cf;  ///< comment flag
94 		char    tf[112]; ///< text field
95 	};
96 #pragma pack(pop)
97 
98 	/// A block of text with basic formatting information
99 	struct EbuFormattedText
100 	{
101 		std::string text; ///< Text in this block
102 		bool underline;   ///< Is this block underlined?
103 		bool italic;      ///< Is this block italic?
104 		bool word_start;  ///< Is it safe to line-wrap between this block and the previous one?
EbuFormattedText__anona71ce3080111::EbuFormattedText105 		EbuFormattedText(std::string t, bool u = false, bool i = false, bool ws = true) : text(std::move(t)), underline(u), italic(i), word_start(ws) { }
106 	};
107 	typedef std::vector<EbuFormattedText> EbuTextRow;
108 
109 	/// Formatting character constants
110 	const unsigned char EBU_FORMAT_ITALIC[]     = "\x81\x80";
111 	const unsigned char EBU_FORMAT_UNDERLINE[]  = "\x83\x82";
112 	const unsigned char EBU_FORMAT_LINEBREAK    = '\x8a';
113 	const unsigned char EBU_FORMAT_UNUSED_SPACE = '\x8f';
114 
115 	/// intermediate format
116 	class EbuSubtitle
117 	{
ProcessOverrides(AssDialogueBlockOverride * ob,bool & underline,bool & italic,int & align,bool style_underline,bool style_italic)118 		void ProcessOverrides(AssDialogueBlockOverride *ob, bool &underline, bool &italic, int &align, bool style_underline, bool style_italic)
119 		{
120 			for (auto const& t : ob->Tags)
121 			{
122 				if (t.Name == "\\u")
123 					underline = t.Params[0].Get<bool>(style_underline);
124 				else if (t.Name == "\\i")
125 					italic = t.Params[0].Get<bool>(style_italic);
126 				else if (t.Name == "\\an")
127 					align = t.Params[0].Get<int>(align);
128 				else if (t.Name == "\\a" && !t.Params[0].omitted)
129 					align = AssStyle::SsaToAss(t.Params[0].Get<int>());
130 			}
131 		}
132 
SetAlignment(int ass_alignment)133 		void SetAlignment(int ass_alignment)
134 		{
135 			if (ass_alignment < 1 || ass_alignment > 9)
136 				ass_alignment = 2;
137 
138 			vertical_position = static_cast<VerticalPosition>(ass_alignment / 3);
139 			justification_code = static_cast<JustificationCode>((ass_alignment - 1) % 3 + 1);
140 		}
141 
142 	public:
143 		enum CumulativeStatus
144 		{
145 			NotCumulative    = 0,
146 			CumulativeStart  = 1,
147 			CulumativeMiddle = 2,
148 			CumulativeEnd    = 3
149 		};
150 
151 		enum JustificationCode
152 		{
153 			UnchangedPresentation = 0,
154 			JustifyLeft           = 1,
155 			JustifyCentre         = 2,
156 			JustifyRight          = 3
157 		};
158 
159 		// note: not set to constants from spec
160 		enum VerticalPosition
161 		{
162 			PositionTop    = 2,
163 			PositionMiddle = 1,
164 			PositionBottom = 0
165 		};
166 
167 		int group_number = 0; ///< always 0 for compat
168 		/// subtitle number is assigned when generating blocks
169 		CumulativeStatus cumulative_status = NotCumulative; ///< always NotCumulative for compat
170 		int time_in = 0;       ///< frame number
171 		int time_out = 0;      ///< frame number
172 		bool comment_flag = false; ///< always false for compat
173 		JustificationCode justification_code = JustifyCentre; ///< never Unchanged presentation for compat
174 		VerticalPosition vertical_position = PositionBottom;   ///< translated to row on tti conversion
175 		std::vector<EbuTextRow> text_rows;    ///< text split into rows, still unicode
176 
SplitLines(int max_width,int split_type)177 		void SplitLines(int max_width, int split_type)
178 		{
179 			// split_type is an SSA wrap style number
180 			if (split_type == 2) return; // no wrapping here!
181 			if (split_type < 0) return;
182 			if (split_type > 4) return;
183 
184 			std::vector<EbuTextRow> new_text;
185 			new_text.reserve(text_rows.size());
186 
187 			for (auto const& row : text_rows)
188 			{
189 				// Get lengths of each word
190 				std::vector<size_t> word_lengths;
191 				for (auto const& cur_block : row)
192 				{
193 					if (cur_block.word_start)
194 						word_lengths.push_back(0);
195 					word_lengths.back() += cur_block.text.size();
196 				}
197 
198 				std::vector<size_t> split_points = agi::get_wrap_points(word_lengths, (size_t)max_width, (agi::WrapMode)split_type);
199 
200 				if (split_points.empty())
201 				{
202 					// Line doesn't need splitting, so copy straight over
203 					new_text.push_back(row);
204 					continue;
205 				}
206 
207 				// Apply the splits
208 				new_text.emplace_back();
209 				size_t cur_word = 0;
210 				size_t split_point = 0;
211 				for (auto const& cur_block : row)
212 				{
213 					if (cur_block.word_start && split_point < split_points.size())
214 					{
215 						if (split_points[split_point] == cur_word)
216 						{
217 							new_text.emplace_back();
218 							++split_point;
219 						}
220 						++cur_word;
221 					}
222 
223 					new_text.back().push_back(cur_block);
224 				}
225 			}
226 
227 			// replace old text
228 			swap(text_rows, new_text);
229 		}
230 
CheckLineLengths(int max_width) const231 		bool CheckLineLengths(int max_width) const
232 		{
233 			for (auto const& row : text_rows)
234 			{
235 				int line_length = 0;
236 				for (auto const& block : row)
237 					line_length += block.text.size();
238 
239 				if (line_length > max_width)
240 					// early return as soon as any line is over length
241 					return false;
242 			}
243 			// no lines failed
244 			return true;
245 		}
246 
SetTextFromAss(AssDialogue * line,bool style_underline,bool style_italic,int align,int wrap_mode)247 		void SetTextFromAss(AssDialogue *line, bool style_underline, bool style_italic, int align, int wrap_mode)
248 		{
249 			text_rows.clear();
250 			text_rows.emplace_back();
251 
252 			// current row being worked on
253 			EbuTextRow *cur_row = &text_rows.back();
254 
255 			// create initial text part
256 			cur_row->emplace_back("", style_underline, style_italic, true);
257 
258 			bool underline = style_underline, italic = style_italic;
259 
260 			for (auto& b : line->ParseTags())
261 			{
262 				switch (b->GetType())
263 				{
264 					case AssBlockType::PLAIN:
265 					// find special characters and convert them
266 					{
267 						std::string text = b->GetText();
268 
269 						boost::replace_all(text, "\\t", " ");
270 
271 						size_t start = 0;
272 						for (size_t i = 0; i < text.size(); ++i)
273 						{
274 							if (text[i] != ' ' && (i + 1 >= text.size() || text[i] != '\\' || (text[i + 1] != 'N' && text[i + 1] != 'n')))
275 								continue;
276 
277 							// add first part of text to current part
278 							cur_row->back().text.append(begin(text) + start, begin(text) + i);
279 
280 							// process special character
281 							if (text[i] == '\\' && (text[i + 1] == 'N' || wrap_mode == 1))
282 							{
283 								// create a new row with current style
284 								text_rows.emplace_back();
285 								cur_row = &text_rows.back();
286 								cur_row->emplace_back("", underline, italic, true);
287 							}
288 							else // if (substr == " " || substr == "\\n")
289 							{
290 								cur_row->back().text.append(" ");
291 								cur_row->emplace_back("", underline, italic, true);
292 							}
293 
294 							if (text[i] == '\\')
295 								start = ++i + 1;
296 							else
297 								start = i;
298 						}
299 
300 						// add the remaining text
301 						cur_row->back().text.append(begin(text) + start, end(text));
302 
303 						// convert \h to regular spaces
304 						// done after parsing so that words aren't split on \h
305 						boost::replace_all(cur_row->back().text, "\\h", " ");
306 					}
307 					break;
308 
309 					case AssBlockType::OVERRIDE:
310 					// find relevant tags and process them
311 					{
312 						AssDialogueBlockOverride *ob = static_cast<AssDialogueBlockOverride*>(b.get());
313 						ob->ParseTags();
314 						ProcessOverrides(ob, underline, italic, align, style_underline, style_italic);
315 
316 						// apply any changes
317 						if (underline != cur_row->back().underline || italic != cur_row->back().italic)
318 						{
319 							if (cur_row->back().text.empty())
320 							{
321 								// current part is empty, we can safely change formatting on it
322 								cur_row->back().underline = underline;
323 								cur_row->back().italic = italic;
324 							}
325 							else
326 							{
327 								// create a new empty part with new style
328 								cur_row->emplace_back("", underline, italic, false);
329 							}
330 						}
331 					}
332 					break;
333 
334 				default:
335 					// ignore block, we don't want to output it (drawing or comment)
336 					break;
337 				}
338 			}
339 
340 			SetAlignment(align);
341 		}
342 	};
343 
convert_subtitles(AssFile & copy,EbuExportSettings const & export_settings)344 	std::vector<EbuSubtitle> convert_subtitles(AssFile &copy, EbuExportSettings const& export_settings)
345 	{
346 		SubtitleFormat::StripComments(copy);
347 		copy.Sort();
348 		SubtitleFormat::RecombineOverlaps(copy);
349 		SubtitleFormat::MergeIdentical(copy);
350 
351 		int line_wrap_type = copy.GetScriptInfoAsInt("WrapStyle");
352 
353 		agi::vfr::Framerate fps = export_settings.GetFramerate();
354 		EbuTimecode tcofs = export_settings.timecode_offset;
355 		int timecode_bias = fps.FrameAtSmpte(tcofs.h, tcofs.m, tcofs.s, tcofs.s);
356 
357 		AssStyle default_style;
358 		std::vector<EbuSubtitle> subs_list;
359 		subs_list.reserve(copy.Events.size());
360 
361 		// convert to intermediate format
362 		for (auto& line : copy.Events)
363 		{
364 			// add a new subtitle and work on it
365 			subs_list.emplace_back();
366 			EbuSubtitle &imline = subs_list.back();
367 
368 			// some defaults for compatibility
369 			imline.group_number = 0;
370 			imline.comment_flag = false;
371 			imline.cumulative_status = EbuSubtitle::NotCumulative;
372 
373 			// convert times
374 			imline.time_in = fps.FrameAtTime(line.Start) + timecode_bias;
375 			imline.time_out = fps.FrameAtTime(line.End) + timecode_bias;
376 			if (export_settings.inclusive_end_times)
377 				// cheap and possibly wrong way to ensure exclusive times, subtract one frame from end time
378 				imline.time_out -= 1;
379 
380 			// convert alignment from style
381 			AssStyle *style = copy.GetStyle(line.Style);
382 			if (!style)
383 				style = &default_style;
384 
385 			// add text, translate formatting
386 			imline.SetTextFromAss(&line, style->underline, style->italic, style->alignment, line_wrap_type);
387 
388 			// line breaking handling
389 			if (export_settings.line_wrapping_mode == EbuExportSettings::AutoWrap)
390 				imline.SplitLines(export_settings.max_line_length, line_wrap_type);
391 			else if (export_settings.line_wrapping_mode == EbuExportSettings::AutoWrapBalance)
392 				imline.SplitLines(export_settings.max_line_length, agi::Wrap_Balanced);
393 			else if (!imline.CheckLineLengths(export_settings.max_line_length))
394 			{
395 				if (export_settings.line_wrapping_mode == EbuExportSettings::AbortOverLength)
396 					throw Ebu3264SubtitleFormat::ConversionFailed(agi::format(_("Line over maximum length: %s"), line.Text));
397 				else // skip over-long lines
398 					subs_list.pop_back();
399 			}
400 		}
401 
402 		// produce an empty line if there are none
403 		// (it still has to contain a space to not get ignored)
404 		if (subs_list.empty())
405 		{
406 			subs_list.emplace_back();
407 			subs_list.back().text_rows.emplace_back();
408 			subs_list.back().text_rows.back().emplace_back(" ");
409 		}
410 
411 		return subs_list;
412 	}
413 
convert_subtitle_line(EbuSubtitle const & sub,agi::charset::IconvWrapper * encoder,bool enable_formatting)414 	std::string convert_subtitle_line(EbuSubtitle const& sub, agi::charset::IconvWrapper *encoder, bool enable_formatting)
415 	{
416 		std::string fullstring;
417 		for (auto const& row : sub.text_rows)
418 		{
419 			if (!fullstring.empty())
420 				fullstring += EBU_FORMAT_LINEBREAK;
421 
422 			// formatting is reset at the start of every row, so keep track per row
423 			bool underline = false, italic = false;
424 			for (auto const& block : row)
425 			{
426 				if (enable_formatting)
427 				{
428 					// insert codes for changed formatting
429 					if (underline != block.underline)
430 						fullstring += EBU_FORMAT_UNDERLINE[block.underline];
431 					if (italic != block.italic)
432 						fullstring += EBU_FORMAT_ITALIC[block.italic];
433 
434 					underline = block.underline;
435 					italic = block.italic;
436 				}
437 
438 				// convert text to specified encoding
439 				fullstring += encoder->Convert(block.text);
440 			}
441 		}
442 		return fullstring;
443 	}
444 
smpte_at_frame(agi::vfr::Framerate const & fps,int frame,EbuTimecode & tc)445 	void smpte_at_frame(agi::vfr::Framerate const& fps, int frame, EbuTimecode &tc)
446 	{
447 		int h=0, m=0, s=0, f=0;
448 		fps.SmpteAtFrame(frame, &h, &m, &s, &f);
449 		tc.h = h;
450 		tc.m = m;
451 		tc.s = s;
452 		tc.f = f;
453 	}
454 
create_blocks(std::vector<EbuSubtitle> const & subs_list,EbuExportSettings const & export_settings)455 	std::vector<BlockTTI> create_blocks(std::vector<EbuSubtitle> const& subs_list, EbuExportSettings const& export_settings)
456 	{
457 		auto encoder = export_settings.GetTextEncoder();
458 		auto fps = export_settings.GetFramerate();
459 
460 		// Teletext captions are 1-23; Open subtitles are 0-99
461 		uint8_t min_row = 0;
462 		uint8_t max_row = 100;
463 		if (export_settings.display_standard != EbuExportSettings::DSC_Open) {
464 			min_row = 1;
465 			max_row = 24;
466 		}
467 
468 		uint16_t subtitle_number = 0;
469 
470 		std::vector<BlockTTI> tti;
471 		tti.reserve(subs_list.size());
472 		for (auto const& sub : subs_list)
473 		{
474 			std::string fullstring = convert_subtitle_line(sub, encoder.get(),
475 				export_settings.display_standard == EbuExportSettings::DSC_Open);
476 
477 			// construct a base block that can be copied and filled
478 			BlockTTI base;
479 			base.sgn = sub.group_number;
480 			base.sn = subtitle_number++;
481 			base.ebn = 255;
482 			base.cf = sub.comment_flag;
483 			memset(base.tf, EBU_FORMAT_UNUSED_SPACE, sizeof(base.tf));
484 			smpte_at_frame(fps, sub.time_in, base.tci);
485 			smpte_at_frame(fps, sub.time_out, base.tco);
486 			base.cs = sub.cumulative_status;
487 
488 			if (export_settings.translate_alignments)
489 			{
490 				// vertical position
491 				if (sub.vertical_position == EbuSubtitle::PositionTop)
492 					base.vp = min_row;
493 				else if (sub.vertical_position == EbuSubtitle::PositionMiddle)
494 					base.vp = std::min<uint8_t>(min_row, max_row / 2 - (max_row / 5 * sub.text_rows.size()));
495 				else //if (sub.vertical_position == EbuSubtitle::PositionBottom)
496 					base.vp = max_row - 1;
497 
498 				base.jc = sub.justification_code;
499 			}
500 			else
501 			{
502 				base.vp = max_row - 1;
503 				base.jc = EbuSubtitle::JustifyCentre;
504 			}
505 
506 			// produce blocks from string
507 			static const size_t block_size = sizeof(((BlockTTI*)nullptr)->tf);
508 			uint8_t num_blocks = 0;
509 			for (size_t pos = 0; pos < fullstring.size(); pos += block_size)
510 			{
511 				size_t bytes_remaining = fullstring.size() - pos;
512 
513 				tti.push_back(base);
514 				// write an extension block number if the remaining text doesn't fit in the block
515 				tti.back().ebn = bytes_remaining >= block_size ? num_blocks++ : 255;
516 
517 				std::copy(&fullstring[pos], &fullstring[pos + std::min(block_size, bytes_remaining)], tti.back().tf);
518 
519 				// Write another block for the terminator if we exactly used up
520 				// the last block
521 				if (bytes_remaining == block_size)
522 					tti.push_back(base);
523 			}
524 		}
525 
526 		return tti;
527 	}
528 
529 #ifdef _MSC_VER
530 #define snprintf _snprintf
531 #endif
532 
create_header(AssFile const & copy,EbuExportSettings const & export_settings)533 	BlockGSI create_header(AssFile const& copy, EbuExportSettings const& export_settings)
534 	{
535 		std::string scriptinfo_title = copy.GetScriptInfo("Title");
536 		std::string scriptinfo_translation = copy.GetScriptInfo("Original Translation");
537 		std::string scriptinfo_editing = copy.GetScriptInfo("Original Editing");
538 
539 		agi::charset::IconvWrapper gsi_encoder("UTF-8", "CP850");
540 
541 		BlockGSI gsi;
542 		memset(&gsi, 0x20, sizeof(gsi)); // fill with spaces
543 		memcpy(gsi.cpn, "850", 3);
544 		switch (export_settings.tv_standard)
545 		{
546 			case EbuExportSettings::STL23:
547 			case EbuExportSettings::STL24:
548 				memcpy(gsi.dfc, "STL24.01", 8);
549 				break;
550 			case EbuExportSettings::STL29:
551 			case EbuExportSettings::STL29drop:
552 			case EbuExportSettings::STL30:
553 				memcpy(gsi.dfc, "STL30.01", 8);
554 				break;
555 			case EbuExportSettings::STL25:
556 			default:
557 				memcpy(gsi.dfc, "STL25.01", 8);
558 				break;
559 		}
560 		gsi.dsc = '0' + (int)export_settings.display_standard;
561 		gsi.cct[0] = '0';
562 		gsi.cct[1] = '0' + (int)export_settings.text_encoding;
563 		if (export_settings.text_encoding == EbuExportSettings::utf8)
564 			memcpy(gsi.cct, "U8", 2);
565 		memcpy(gsi.lc, "00", 2);
566 		gsi_encoder.Convert(scriptinfo_title.c_str(), scriptinfo_title.size(), gsi.opt, 32);
567 		gsi_encoder.Convert(scriptinfo_translation.c_str(), scriptinfo_translation.size(), gsi.tn, 32);
568 		{
569 			char buf[20];
570 			time_t now;
571 			time(&now);
572 			tm *thetime = localtime(&now);
573 			strftime(buf, 20, "AGI-%y%m%d%H%M%S", thetime);
574 			memcpy(gsi.slr, buf, 16);
575 			strftime(buf, 20, "%y%m%d", thetime);
576 			memcpy(gsi.cd, buf, 6);
577 			memcpy(gsi.rd, buf, 6);
578 			memcpy(gsi.rn, "00", 2);
579 			memcpy(gsi.tng, "001", 3);
580 			snprintf(gsi.mnc, 2, "%02u", (unsigned int)export_settings.max_line_length);
581 			memcpy(gsi.mnr, "99", 2);
582 			gsi.tcs = '1';
583 			EbuTimecode tcofs = export_settings.timecode_offset;
584 			snprintf(gsi.tcp, 8, "%02u%02u%02u%02u", (unsigned int)tcofs.h, (unsigned int)tcofs.m, (unsigned int)tcofs.s, (unsigned int)tcofs.s);
585 		}
586 		gsi.tnd = '1';
587 		gsi.dsn = '1';
588 		memcpy(gsi.co, "NTZ", 3); // neutral zone!
589 		gsi_encoder.Convert(scriptinfo_editing.c_str(), scriptinfo_editing.size(), gsi.en, 32);
590 		if (export_settings.text_encoding == EbuExportSettings::utf8)
591 			strncpy(gsi.uda, "This file was exported by Aegisub using non-standard UTF-8 encoding for the subtitle blocks. The TTI.TF field contains UTF-8-encoded text interspersed with the standard formatting codes, which are not encoded. GSI.CCT is set to 'U8' to signify this.", sizeof(gsi.uda));
592 
593 		return gsi;
594 	}
595 
get_export_config(wxWindow * parent)596 	EbuExportSettings get_export_config(wxWindow *parent)
597 	{
598 		EbuExportSettings s("Subtitle Format/EBU STL");
599 
600 		// Disable the busy cursor set by the exporter while the dialog is visible
601 		wxEndBusyCursor();
602 		int res = ShowEbuExportConfigurationDialog(parent, s);
603 		wxBeginBusyCursor();
604 
605 		if (res != wxID_OK)
606 			throw agi::UserCancelException("EBU/STL export");
607 
608 		s.Save();
609 		return s;
610 	}
611 
612 } // namespace {
613 
Ebu3264SubtitleFormat()614 Ebu3264SubtitleFormat::Ebu3264SubtitleFormat()
615 : SubtitleFormat("EBU subtitling data exchange format (EBU tech 3264, 1991)")
616 {
617 }
618 
WriteFile(const AssFile * src,agi::fs::path const & filename,agi::vfr::Framerate const & fps,std::string const &) const619 void Ebu3264SubtitleFormat::WriteFile(const AssFile *src, agi::fs::path const& filename, agi::vfr::Framerate const& fps, std::string const&) const
620 {
621 	// collect data from user
622 	EbuExportSettings export_settings = get_export_config(nullptr);
623 	AssFile copy(*src);
624 
625 	std::vector<EbuSubtitle> subs_list = convert_subtitles(copy, export_settings);
626 	std::vector<BlockTTI> tti = create_blocks(subs_list, export_settings);
627 	BlockGSI gsi = create_header(copy, export_settings);
628 
629 	BlockTTI &block0 = tti.front();
630 	snprintf(gsi.tcf, 8, "%02u%02u%02u%02u", (unsigned int)block0.tci.h, (unsigned int)block0.tci.m, (unsigned int)block0.tci.s, (unsigned int)block0.tci.f);
631 	snprintf(gsi.tnb, 5, "%5u", (unsigned int)tti.size());
632 	snprintf(gsi.tns, 5, "%5u", (unsigned int)subs_list.size());
633 
634 	// write file
635 	agi::io::Save f(filename, true);
636 	f.Get().write((const char *)&gsi, sizeof(gsi));
637 	for (auto const& block : tti)
638 		f.Get().write((const char *)&block, sizeof(block));
639 }
640