Port the EBU STL (tech 3264) subtitle format from 2.1.9

Split the configuration dialog off into its own file and mostly decouple
it from the subtitle format.

Save last used export settings to options and restore them the next time
the dialog is opened.

Use libaegisub for charset conversion and IO rather than wxWidgets.

Use libaegisub's line-wrapping logic and finish implementing all of the
various wrapping modes.

Make unchecking the "Translate alignments" checkbox do something.

Originally committed to SVN as r6636.
This commit is contained in:
Thomas Goyne 2012-03-29 19:05:26 +00:00
parent 028fd3b4ba
commit 4294e5857d
12 changed files with 1150 additions and 16 deletions

View file

@ -1035,6 +1035,14 @@
RelativePath="..\..\src\dialog_export.h" RelativePath="..\..\src\dialog_export.h"
> >
</File> </File>
<File
RelativePath="..\..\src\dialog_export_ebu3264.cpp"
>
</File>
<File
RelativePath="..\..\src\dialog_export_ebu3264.h"
>
</File>
<File <File
RelativePath="..\..\src\dialog_fonts_collector.cpp" RelativePath="..\..\src\dialog_fonts_collector.cpp"
> >
@ -1439,6 +1447,14 @@
RelativePath="..\..\src\subtitle_format_ass.h" RelativePath="..\..\src\subtitle_format_ass.h"
> >
</File> </File>
<File
RelativePath="..\..\src\subtitle_format_ebu3264.cpp"
>
</File>
<File
RelativePath="..\..\src\subtitle_format_ebu3264.h"
>
</File>
<File <File
RelativePath="..\..\src\subtitle_format_encore.cpp" RelativePath="..\..\src\subtitle_format_encore.cpp"
> >

View file

@ -109,6 +109,7 @@
<ClInclude Include="$(SrcDir)dialog_detached_video.h" /> <ClInclude Include="$(SrcDir)dialog_detached_video.h" />
<ClInclude Include="$(SrcDir)dialog_dummy_video.h" /> <ClInclude Include="$(SrcDir)dialog_dummy_video.h" />
<ClInclude Include="$(SrcDir)dialog_export.h" /> <ClInclude Include="$(SrcDir)dialog_export.h" />
<ClInclude Include="$(SrcDir)dialog_export_ebu3264.h" />
<ClInclude Include="$(SrcDir)dialog_fonts_collector.h" /> <ClInclude Include="$(SrcDir)dialog_fonts_collector.h" />
<ClInclude Include="$(SrcDir)dialog_jumpto.h" /> <ClInclude Include="$(SrcDir)dialog_jumpto.h" />
<ClInclude Include="$(SrcDir)dialog_kara_timing_copy.h" /> <ClInclude Include="$(SrcDir)dialog_kara_timing_copy.h" />
@ -170,6 +171,7 @@
<ClInclude Include="$(SrcDir)subtitles_provider_libass.h" /> <ClInclude Include="$(SrcDir)subtitles_provider_libass.h" />
<ClInclude Include="$(SrcDir)subtitle_format.h" /> <ClInclude Include="$(SrcDir)subtitle_format.h" />
<ClInclude Include="$(SrcDir)subtitle_format_ass.h" /> <ClInclude Include="$(SrcDir)subtitle_format_ass.h" />
<ClInclude Include="$(SrcDir)subtitle_format_ebu3264.h" />
<ClInclude Include="$(SrcDir)subtitle_format_encore.h" /> <ClInclude Include="$(SrcDir)subtitle_format_encore.h" />
<ClInclude Include="$(SrcDir)subtitle_format_microdvd.h" /> <ClInclude Include="$(SrcDir)subtitle_format_microdvd.h" />
<ClInclude Include="$(SrcDir)subtitle_format_mkv.h" /> <ClInclude Include="$(SrcDir)subtitle_format_mkv.h" />
@ -293,6 +295,7 @@
<ClCompile Include="$(SrcDir)dialog_detached_video.cpp" /> <ClCompile Include="$(SrcDir)dialog_detached_video.cpp" />
<ClCompile Include="$(SrcDir)dialog_dummy_video.cpp" /> <ClCompile Include="$(SrcDir)dialog_dummy_video.cpp" />
<ClCompile Include="$(SrcDir)dialog_export.cpp" /> <ClCompile Include="$(SrcDir)dialog_export.cpp" />
<ClCompile Include="$(SrcDir)dialog_export_ebu3264.cpp" />
<ClCompile Include="$(SrcDir)dialog_fonts_collector.cpp" /> <ClCompile Include="$(SrcDir)dialog_fonts_collector.cpp" />
<ClCompile Include="$(SrcDir)dialog_jumpto.cpp" /> <ClCompile Include="$(SrcDir)dialog_jumpto.cpp" />
<ClCompile Include="$(SrcDir)dialog_kara_timing_copy.cpp" /> <ClCompile Include="$(SrcDir)dialog_kara_timing_copy.cpp" />
@ -360,6 +363,7 @@
<ClCompile Include="$(SrcDir)subtitles_provider_libass.cpp" /> <ClCompile Include="$(SrcDir)subtitles_provider_libass.cpp" />
<ClCompile Include="$(SrcDir)subtitle_format.cpp" /> <ClCompile Include="$(SrcDir)subtitle_format.cpp" />
<ClCompile Include="$(SrcDir)subtitle_format_ass.cpp" /> <ClCompile Include="$(SrcDir)subtitle_format_ass.cpp" />
<ClCompile Include="$(SrcDir)subtitle_format_ebu3264.cpp" />
<ClCompile Include="$(SrcDir)subtitle_format_encore.cpp" /> <ClCompile Include="$(SrcDir)subtitle_format_encore.cpp" />
<ClCompile Include="$(SrcDir)subtitle_format_microdvd.cpp" /> <ClCompile Include="$(SrcDir)subtitle_format_microdvd.cpp" />
<ClCompile Include="$(SrcDir)subtitle_format_mkv.cpp" /> <ClCompile Include="$(SrcDir)subtitle_format_mkv.cpp" />

View file

@ -297,6 +297,9 @@
<ClInclude Include="$(SrcDir)dialog_export.h"> <ClInclude Include="$(SrcDir)dialog_export.h">
<Filter>Features\Export</Filter> <Filter>Features\Export</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="$(SrcDir)dialog_export_ebu3264.h">
<Filter>Features\Export</Filter>
</ClInclude>
<ClInclude Include="$(SrcDir)export_framerate.h"> <ClInclude Include="$(SrcDir)export_framerate.h">
<Filter>Features\Export</Filter> <Filter>Features\Export</Filter>
</ClInclude> </ClInclude>
@ -351,6 +354,9 @@
<ClInclude Include="$(SrcDir)subtitle_format_ass.h"> <ClInclude Include="$(SrcDir)subtitle_format_ass.h">
<Filter>Subtitle formats</Filter> <Filter>Subtitle formats</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="$(SrcDir)subtitle_format_ebu3264.h">
<Filter>Subtitle formats</Filter>
</ClInclude>
<ClInclude Include="$(SrcDir)subtitle_format_encore.h"> <ClInclude Include="$(SrcDir)subtitle_format_encore.h">
<Filter>Subtitle formats</Filter> <Filter>Subtitle formats</Filter>
</ClInclude> </ClInclude>
@ -788,6 +794,9 @@
<ClCompile Include="$(SrcDir)subtitle_format_ass.cpp"> <ClCompile Include="$(SrcDir)subtitle_format_ass.cpp">
<Filter>Subtitle formats</Filter> <Filter>Subtitle formats</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="$(SrcDir)subtitle_format_ebu3264.cpp">
<Filter>Subtitle formats</Filter>
</ClCompile>
<ClCompile Include="$(SrcDir)subtitle_format_encore.cpp"> <ClCompile Include="$(SrcDir)subtitle_format_encore.cpp">
<Filter>Subtitle formats</Filter> <Filter>Subtitle formats</Filter>
</ClCompile> </ClCompile>
@ -881,6 +890,9 @@
<ClCompile Include="$(SrcDir)dialog_export.cpp"> <ClCompile Include="$(SrcDir)dialog_export.cpp">
<Filter>Features\Export</Filter> <Filter>Features\Export</Filter>
</ClCompile> </ClCompile>
<ClCompile Include="$(SrcDir)dialog_export_ebu3264.cpp">
<Filter>Features\Export</Filter>
</ClCompile>
<ClCompile Include="$(SrcDir)export_fixstyle.cpp"> <ClCompile Include="$(SrcDir)export_fixstyle.cpp">
<Filter>Features\Export</Filter> <Filter>Features\Export</Filter>
</ClCompile> </ClCompile>

View file

@ -166,6 +166,7 @@ SRC += \
dialog_detached_video.cpp \ dialog_detached_video.cpp \
dialog_dummy_video.cpp \ dialog_dummy_video.cpp \
dialog_export.cpp \ dialog_export.cpp \
dialog_export_ebu3264.cpp \
dialog_fonts_collector.cpp \ dialog_fonts_collector.cpp \
dialog_jumpto.cpp \ dialog_jumpto.cpp \
dialog_kara_timing_copy.cpp \ dialog_kara_timing_copy.cpp \
@ -221,6 +222,7 @@ SRC += \
subs_preview.cpp \ subs_preview.cpp \
subtitle_format.cpp \ subtitle_format.cpp \
subtitle_format_ass.cpp \ subtitle_format_ass.cpp \
subtitle_format_ebu3264.cpp \
subtitle_format_encore.cpp \ subtitle_format_encore.cpp \
subtitle_format_microdvd.cpp \ subtitle_format_microdvd.cpp \
subtitle_format_mkv.cpp \ subtitle_format_mkv.cpp \

View file

@ -193,6 +193,7 @@
#include <wx/validate.h> #include <wx/validate.h>
#include <wx/valgen.h> #include <wx/valgen.h>
#include <wx/valnum.h> #include <wx/valnum.h>
#include <wx/valgen.h>
#include <wx/valtext.h> #include <wx/valtext.h>
#include <wx/wfstream.h> #include <wx/wfstream.h>
#include <wx/window.h> #include <wx/window.h>

View file

@ -0,0 +1,258 @@
// Copyright (c) 2011 Niels Martin Hansen <nielsm@aegisub.org>
// Copyright (c) 2012 Thomas Goyne <plorkyeran@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Aegisub Project http://www.aegisub.org/
/// @file dialog_export_ebu3264.cpp
/// @see dialog_export_ebu3264.h
/// @ingroup subtitle_io export
#include "config.h"
#include "dialog_export_ebu3264.h"
#include <libaegisub/charset_conv.h>
#include "main.h"
#include "text_file_writer.h"
#ifndef AGI_PRE
#include <wx/checkbox.h>
#include <wx/combobox.h>
#include <wx/msgdlg.h>
#include <wx/radiobox.h>
#include <wx/regex.h>
#include <wx/sizer.h>
#include <wx/spinctrl.h>
#include <wx/statbox.h>
#include <wx/stattext.h>
#include <wx/textctrl.h>
#include <wx/valgen.h>
#endif
namespace {
const char timecode_regex[] = "([[:digit:]]{2}):([[:digit:]]{2}):([[:digit:]]{2}):([[:digit:]]{2})";
/// Validator for SMPTE timecodes
class TimecodeValidator : public wxValidator {
wxRegEx re;
EbuTimecode *value;
wxTextCtrl *GetCtrl() const { return dynamic_cast<wxTextCtrl*>(GetWindow()); }
bool TransferToWindow() {
wxTextCtrl *ctrl = GetCtrl();
if (!ctrl) return false;
ctrl->SetValue(wxString::Format("%02d:%02d:%02d:%02d", (int)value->h, (int)value->m, (int)value->s, (int)value->f));
return true;
}
bool TransferFromWindow() {
wxTextCtrl *ctrl = GetCtrl();
if (!ctrl) return false;
wxString str = ctrl->GetValue();
if (re.Matches(str)) {
long h, m, s, f;
re.GetMatch(str, 1).ToLong(&h);
re.GetMatch(str, 2).ToLong(&m);
re.GetMatch(str, 3).ToLong(&s);
re.GetMatch(str, 4).ToLong(&f);
value->h = h;
value->m = m;
value->s = s;
value->f = f;
return true;
}
return false;
}
bool Validate(wxWindow *parent) {
wxTextCtrl *ctrl = GetCtrl();
if (!ctrl) return false;
if (!re.Matches(ctrl->GetValue())) {
wxMessageBox(_("Time code offset in incorrect format. Ensure it is entered as four groups of two digits separated by colons."), _("EBU STL export"), wxICON_EXCLAMATION);
return false;
}
return true;
}
wxObject *Clone() const { return new TimecodeValidator(*this); }
public:
TimecodeValidator(EbuTimecode *target)
: re(timecode_regex)
, value(target)
{
assert(target);
}
TimecodeValidator(TimecodeValidator const& other)
: wxValidator()
, re(timecode_regex)
, value(other.value)
{
}
};
} // namespace {
EbuExportConfigurationDialog::EbuExportConfigurationDialog(wxWindow *owner, EbuExportSettings &s)
: wxDialog(owner, -1, _("Export to EBU STL format"))
{
wxString tv_standards[] = {
_("23.976 fps (non-standard, STL24.01)"),
_("24 fps (non-standard, STL24.01)"),
_("25 fps (STL25.01)"),
_("29.97 fps (non-dropframe, STL30.01)"),
_("29.97 fps (dropframe, STL30.01)"),
_("30 fps (STL30.01)")
};
wxRadioBox *tv_standard_box = new wxRadioBox(this, -1, _("TV standard"), wxDefaultPosition, wxDefaultSize, 6, tv_standards, 0, wxRA_SPECIFY_ROWS);
wxStaticBox *timecode_control_box = new wxStaticBox(this, -1, _("Time codes"));
wxTextCtrl *timecode_offset_entry = new wxTextCtrl(this, -1, "00:00:00:00");
wxCheckBox *inclusive_end_times_check = new wxCheckBox(this, -1, _("Out-times are inclusive"));
wxString text_encodings[] = {
_("ISO 6937-2 (Latin/Western Europe)"),
_("ISO 8859-5 (Cyrillic)"),
_("ISO 8859-6 (Arabic)"),
_("ISO 8859-7 (Greek)"),
_("ISO 8859-8 (Hebrew)"),
_("UTF-8 Unicode (non-standard)")
};
wxRadioBox *text_encoding_box = new wxRadioBox(this, -1, _("Text encoding"), wxDefaultPosition, wxDefaultSize, 6, text_encodings, 0, wxRA_SPECIFY_ROWS);
wxString wrap_modes[] = {
_("Automatically wrap long lines (ASS)"),
_("Automatically wrap long lines (Balanced)"),
_("Abort if any lines are too long"),
_("Skip lines that are too long")
};
wxStaticBox *text_formatting_box = new wxStaticBox(this, -1, _("Text formatting"));
wxSpinCtrl *max_line_length_ctrl = new wxSpinCtrl(this, -1, wxString(), wxDefaultPosition, wxSize(65, -1));
wxComboBox *wrap_mode_ctrl = new wxComboBox(this, -1, wrap_modes[0], wxDefaultPosition, wxDefaultSize, 4, wrap_modes, wxCB_DROPDOWN | wxCB_READONLY);
wxCheckBox *translate_alignments_check = new wxCheckBox(this, -1, _("Translate alignments"));
max_line_length_ctrl->SetRange(10, 99);
wxSizer *max_line_length_labelled = new wxBoxSizer(wxHORIZONTAL);
max_line_length_labelled->Add(new wxStaticText(this, -1, _("Max. line length:")), 1, wxALIGN_CENTRE|wxRIGHT, 12);
max_line_length_labelled->Add(max_line_length_ctrl, 0, 0, 0);
wxSizer *timecode_offset_labelled = new wxBoxSizer(wxHORIZONTAL);
timecode_offset_labelled->Add(new wxStaticText(this, -1, _("Time code offset:")), 1, wxALIGN_CENTRE|wxRIGHT, 12);
timecode_offset_labelled->Add(timecode_offset_entry, 0, 0, 0);
wxSizer *text_formatting_sizer = new wxStaticBoxSizer(text_formatting_box, wxVERTICAL);
text_formatting_sizer->Add(max_line_length_labelled, 0, wxEXPAND | (wxALL & ~wxTOP), 6);
text_formatting_sizer->Add(wrap_mode_ctrl, 0, wxEXPAND | (wxALL & ~wxTOP), 6);
text_formatting_sizer->Add(translate_alignments_check, 0, wxEXPAND | (wxALL & ~wxTOP), 6);
wxSizer *timecode_control_sizer = new wxStaticBoxSizer(timecode_control_box, wxVERTICAL);
timecode_control_sizer->Add(timecode_offset_labelled, 0, wxEXPAND | (wxALL & ~wxTOP), 6);
timecode_control_sizer->Add(inclusive_end_times_check, 0, wxEXPAND | (wxALL & ~wxTOP), 6);
wxSizer *left_column = new wxBoxSizer(wxVERTICAL);
left_column->Add(tv_standard_box, 0, wxEXPAND|wxBOTTOM, 6);
left_column->Add(timecode_control_sizer, 0, wxEXPAND, 0);
wxSizer *right_column = new wxBoxSizer(wxVERTICAL);
right_column->Add(text_encoding_box, 0, wxEXPAND|wxBOTTOM, 6);
right_column->Add(text_formatting_sizer, 0, wxEXPAND, 0);
wxSizer *vertical_split_sizer = new wxBoxSizer(wxHORIZONTAL);
vertical_split_sizer->Add(left_column, 0, wxRIGHT, 6);
vertical_split_sizer->Add(right_column, 0, 0, 0);
wxSizer *buttons_sizer = new wxBoxSizer(wxHORIZONTAL);
// Developers are requested to leave this message in! Intentionally not translatable.
wxStaticText *sponsor_label = new wxStaticText(this, -1, "EBU STL format writing sponsored by Bandai");
sponsor_label->Enable(false);
buttons_sizer->Add(sponsor_label, 1, wxALIGN_BOTTOM, 0);
buttons_sizer->Add(CreateStdDialogButtonSizer(wxOK | wxCANCEL), 0, wxLEFT, 6);
wxSizer *main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(vertical_split_sizer, 0, wxEXPAND|wxALL, 12);
main_sizer->Add(buttons_sizer, 0, wxEXPAND | (wxALL & ~wxTOP), 12);
SetSizerAndFit(main_sizer);
CenterOnParent();
// set up validators to move data in and out
tv_standard_box->SetValidator(wxGenericValidator((int*)&s.tv_standard));
text_encoding_box->SetValidator(wxGenericValidator((int*)&s.text_encoding));
translate_alignments_check->SetValidator(wxGenericValidator(&s.translate_alignments));
max_line_length_ctrl->SetValidator(wxGenericValidator(&s.max_line_length));
wrap_mode_ctrl->SetValidator(wxGenericValidator((int*)&s.line_wrapping_mode));
inclusive_end_times_check->SetValidator(wxGenericValidator(&s.inclusive_end_times));
timecode_offset_entry->SetValidator(TimecodeValidator(&s.timecode_offset));
}
agi::vfr::Framerate EbuExportSettings::GetFramerate() const {
switch (tv_standard) {
case STL24: return agi::vfr::Framerate(24, 1);
case STL25: return agi::vfr::Framerate(25, 1);
case STL30: return agi::vfr::Framerate(30, 1);
case STL23: return agi::vfr::Framerate(24000, 1001, false);
case STL29: return agi::vfr::Framerate(30000, 1001, false);
case STL29drop: return agi::vfr::Framerate(30000, 1001);
default: return agi::vfr::Framerate(25, 1);
}
}
agi::charset::IconvWrapper *EbuExportSettings::GetTextEncoder() const {
switch (text_encoding) {
case iso6937_2: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "ISO-6937-2");
case iso8859_5: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "ISO-8859-5");
case iso8859_6: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "ISO-8859-6");
case iso8859_7: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "ISO-8859-7");
case iso8859_8: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "ISO-8859-8");
case utf8: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "utf-8");
default: return new agi::charset::IconvWrapper(wxSTRING_ENCODING, "ISO-8859-1");
}
}
EbuExportSettings::EbuExportSettings(std::string const& prefix)
: prefix(prefix)
, tv_standard((TvStandard)OPT_GET(prefix + "/TV Standard")->GetInt())
, text_encoding((TextEncoding)OPT_GET(prefix + "/Text Encoding")->GetInt())
, max_line_length(OPT_GET(prefix + "/Max Line Length")->GetInt())
, line_wrapping_mode((LineWrappingMode)OPT_GET(prefix + "/Line Wrapping Mode")->GetInt())
, translate_alignments(OPT_GET(prefix + "/Translate Alignments")->GetBool())
, inclusive_end_times(OPT_GET(prefix + "/Inclusive End Times")->GetBool())
{
timecode_offset.h = OPT_GET(prefix + "/Timecode Offset/H")->GetInt();
timecode_offset.m = OPT_GET(prefix + "/Timecode Offset/M")->GetInt();
timecode_offset.s = OPT_GET(prefix + "/Timecode Offset/S")->GetInt();
timecode_offset.f = OPT_GET(prefix + "/Timecode Offset/F")->GetInt();
}
void EbuExportSettings::Save() const {
OPT_SET(prefix + "/TV Standard")->SetInt(tv_standard);
OPT_SET(prefix + "/Text Encoding")->SetInt(text_encoding);
OPT_SET(prefix + "/Max Line Length")->SetInt(max_line_length);
OPT_SET(prefix + "/Line Wrapping Mode")->SetInt(line_wrapping_mode);
OPT_SET(prefix + "/Translate Alignments")->SetBool(translate_alignments);
OPT_SET(prefix + "/Inclusive End Times")->SetBool(inclusive_end_times);
OPT_SET(prefix + "/Timecode Offset/H")->SetInt(timecode_offset.h);
OPT_SET(prefix + "/Timecode Offset/M")->SetInt(timecode_offset.m);
OPT_SET(prefix + "/Timecode Offset/S")->SetInt(timecode_offset.s);
OPT_SET(prefix + "/Timecode Offset/F")->SetInt(timecode_offset.f);
}

View file

@ -0,0 +1,112 @@
// Copyright (c) 2011 Niels Martin Hansen <nielsm@aegisub.org>
// Copyright (c) 2012 Thomas Goyne <plorkyeran@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Aegisub Project http://www.aegisub.org/
/// @file dialog_export_ebu3264.h
/// @see dialog_export_ebu3264.cpp
/// @ingroup subtitle_io export
#ifndef AGI_PRE
#include <wx/dialog.h>
#endif
#include <libaegisub/vfr.h>
namespace agi { namespace charset { class IconvWrapper; } }
#pragma pack(push, 1)
/// A binary timecode representation, packed
struct EbuTimecode {
uint8_t h, m, s, f;
};
#pragma pack(pop)
/// User configuration for EBU Tech 3264-1991 export
class EbuExportSettings {
/// Prefix used for saving to/loading from options
std::string prefix;
public:
/// Frame rate + timecode format
enum TvStandard {
STL23 = 0, ///< 23.976 fps (non-dropframe) (marked as 24)
STL24 = 1, ///< 24 fps (film)
STL25 = 2, ///< 25 fps (PAL)
STL29 = 3, ///< 29.97 fps (non-dropframe) (marked as 30)
STL29drop = 4, ///< 29.97 fps (dropframe) (marked as 30)
STL30 = 5, ///< 30 fps (NTSC monochrome)
};
/// Character sets for subtitle data
enum TextEncoding {
iso6937_2 = 0, ///< latin multibyte
iso8859_5 = 1, ///< cyrillic
iso8859_6 = 2, ///< arabic
iso8859_7 = 3, ///< greek
iso8859_8 = 4, ///< hebrew
utf8 = 5, ///< nonstandard
};
/// Modes for handling lines over the maximum width
enum LineWrappingMode {
AutoWrap = 0, ///< Wrap overly-long lines ASS-style
AutoWrapBalance = 1, ///< Wrap overly-long lines with balanced lines
AbortOverLength = 2, ///< Fail if there are overly-long lines
IgnoreOverLength = 3 ///< Skip overly-long lines
};
/// Which TV standard (frame rate + timecode encoding) to use
TvStandard tv_standard;
/// How to encode subtitle text
TextEncoding text_encoding;
/// Maximum length of rows in subtitles (in characters)
int max_line_length;
/// How to deal with over-length rows
LineWrappingMode line_wrapping_mode;
/// Translate SSA alignments?
bool translate_alignments;
/// Timecode which time 0 in Aegisub corresponds to
EbuTimecode timecode_offset;
/// Are end timecodes inclusive or exclusive?
bool inclusive_end_times;
/// Get the frame rate for the current TV Standard
agi::vfr::Framerate GetFramerate() const;
/// Get a charset encoder for the current text encoding
agi::charset::IconvWrapper *GetTextEncoder() const;
/// Load saved export settings from options
/// @param prefix Option name prefix
EbuExportSettings(std::string const& prefix);
/// Save export settings to options
void Save() const;
};
/// Dialog box for getting an export configuration for EBU Tech 3264-1991
class EbuExportConfigurationDialog : public wxDialog {
public:
/// Constructor
/// @param owner Parent window of the dialog
/// @param s Struct with initial values and to fill with the chosen settings
EbuExportConfigurationDialog(wxWindow *owner, EbuExportSettings &s);
};

View file

@ -385,6 +385,23 @@
} }
}, },
"Subtitle Format" : {
"EBU STL" : {
"Inclusive End Times" : true,
"Line Wrapping Mode" : 1,
"Max Line Length" : 42,
"TV Standard" : 0,
"Text Encoding" : 0,
"Timecode Offset" : {
"H" : 0,
"M" : 0,
"S" : 0,
"F" : 0
},
"Translate Alignments" : true
}
},
"Timing" : { "Timing" : {
"Default Duration" : 2000 "Default Duration" : 2000
}, },

View file

@ -48,6 +48,7 @@
#include "ass_file.h" #include "ass_file.h"
#include "ass_style.h" #include "ass_style.h"
#include "subtitle_format_ass.h" #include "subtitle_format_ass.h"
#include "subtitle_format_ebu3264.h"
#include "subtitle_format_encore.h" #include "subtitle_format_encore.h"
#include "subtitle_format_microdvd.h" #include "subtitle_format_microdvd.h"
#include "subtitle_format_mkv.h" #include "subtitle_format_mkv.h"
@ -100,7 +101,7 @@ bool SubtitleFormat::CanSave(const AssFile *subs) const {
return true; return true;
} }
agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte) const { agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte) {
wxArrayString choices; wxArrayString choices;
// Video FPS // Video FPS
@ -164,7 +165,7 @@ agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte) c
return Framerate(); return Framerate();
} }
void SubtitleFormat::StripTags(LineList &lines) const { void SubtitleFormat::StripTags(LineList &lines) {
for (LineList::iterator cur = lines.begin(); cur != lines.end(); ++cur) { for (LineList::iterator cur = lines.begin(); cur != lines.end(); ++cur) {
if (AssDialogue *current = dynamic_cast<AssDialogue*>(*cur)) { if (AssDialogue *current = dynamic_cast<AssDialogue*>(*cur)) {
current->StripTags(); current->StripTags();
@ -172,7 +173,7 @@ void SubtitleFormat::StripTags(LineList &lines) const {
} }
} }
void SubtitleFormat::ConvertNewlines(LineList &lines, wxString const& newline, bool mergeLineBreaks) const { void SubtitleFormat::ConvertNewlines(LineList &lines, wxString const& newline, bool mergeLineBreaks) {
for (LineList::iterator cur = lines.begin(); cur != lines.end(); ++cur) { for (LineList::iterator cur = lines.begin(); cur != lines.end(); ++cur) {
if (AssDialogue *current = dynamic_cast<AssDialogue*>(*cur)) { if (AssDialogue *current = dynamic_cast<AssDialogue*>(*cur)) {
current->Text.Replace("\\h", " "); current->Text.Replace("\\h", " ");
@ -185,7 +186,7 @@ void SubtitleFormat::ConvertNewlines(LineList &lines, wxString const& newline, b
} }
} }
void SubtitleFormat::StripComments(LineList &lines) const { void SubtitleFormat::StripComments(LineList &lines) {
for (LineList::iterator it = lines.begin(); it != lines.end(); ) { for (LineList::iterator it = lines.begin(); it != lines.end(); ) {
AssDialogue *diag = dynamic_cast<AssDialogue*>(*it); AssDialogue *diag = dynamic_cast<AssDialogue*>(*it);
if (!diag || (!diag->Comment && diag->Text.size())) if (!diag || (!diag->Comment && diag->Text.size()))
@ -197,7 +198,7 @@ void SubtitleFormat::StripComments(LineList &lines) const {
} }
} }
void SubtitleFormat::StripNonDialogue(LineList &lines) const { void SubtitleFormat::StripNonDialogue(LineList &lines) {
for (LineList::iterator it = lines.begin(); it != lines.end(); ) { for (LineList::iterator it = lines.begin(); it != lines.end(); ) {
if (dynamic_cast<AssDialogue*>(*it)) if (dynamic_cast<AssDialogue*>(*it))
++it; ++it;
@ -216,7 +217,7 @@ static bool dialog_start_lt(AssEntry *pos, AssDialogue *to_insert) {
/// @brief Split and merge lines so there are no overlapping lines /// @brief Split and merge lines so there are no overlapping lines
/// ///
/// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge /// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge
void SubtitleFormat::RecombineOverlaps(LineList &lines) const { void SubtitleFormat::RecombineOverlaps(LineList &lines) {
LineList::iterator cur, next = lines.begin(); LineList::iterator cur, next = lines.begin();
cur = next++; cur = next++;
@ -288,7 +289,7 @@ void SubtitleFormat::RecombineOverlaps(LineList &lines) const {
} }
/// @brief Merge identical lines that follow each other /// @brief Merge identical lines that follow each other
void SubtitleFormat::MergeIdentical(LineList &lines) const { void SubtitleFormat::MergeIdentical(LineList &lines) {
LineList::iterator cur, next = lines.begin(); LineList::iterator cur, next = lines.begin();
cur = next++; cur = next++;
@ -313,6 +314,7 @@ std::list<SubtitleFormat*> SubtitleFormat::formats;
void SubtitleFormat::LoadFormats() { void SubtitleFormat::LoadFormats() {
if (formats.empty()) { if (formats.empty()) {
new ASSSubtitleFormat; new ASSSubtitleFormat;
new Ebu3264SubtitleFormat;
new EncoreSubtitleFormat; new EncoreSubtitleFormat;
new MKVSubtitleFormat; new MKVSubtitleFormat;
new MicroDVDSubtitleFormat; new MicroDVDSubtitleFormat;

View file

@ -65,32 +65,31 @@ class SubtitleFormat {
/// List of loaded subtitle formats /// List of loaded subtitle formats
static std::list<SubtitleFormat*> formats; static std::list<SubtitleFormat*> formats;
protected: public:
typedef std::list<AssEntry*> LineList; typedef std::list<AssEntry*> LineList;
/// Strip override tags /// Strip override tags
void StripTags(LineList &lines) const; static void StripTags(LineList &lines);
/// Convert newlines to the specified character(s) /// Convert newlines to the specified character(s)
/// @param lineEnd newline character(s) /// @param lineEnd newline character(s)
/// @param mergeLineBreaks Should multiple consecutive line breaks be merged into one? /// @param mergeLineBreaks Should multiple consecutive line breaks be merged into one?
void ConvertNewlines(LineList &lines, wxString const& newline, bool mergeLineBreaks = true) const; static void ConvertNewlines(LineList &lines, wxString const& newline, bool mergeLineBreaks = true);
/// Remove All commented and empty lines /// Remove All commented and empty lines
void StripComments(LineList &lines) const; static void StripComments(LineList &lines);
/// Remove everything but the dialogue lines /// Remove everything but the dialogue lines
void StripNonDialogue(LineList &lines) const; static void StripNonDialogue(LineList &lines);
/// @brief Split and merge lines so there are no overlapping lines /// @brief Split and merge lines so there are no overlapping lines
/// ///
/// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge /// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge
void RecombineOverlaps(LineList &lines) const; static void RecombineOverlaps(LineList &lines);
/// Merge sequential identical lines /// Merge sequential identical lines
void MergeIdentical(LineList &lines) const; static void MergeIdentical(LineList &lines);
/// Prompt the user for a frame rate to use /// Prompt the user for a frame rate to use
/// @param allow_vfr Include video frame rate as an option even if it's vfr /// @param allow_vfr Include video frame rate as an option even if it's vfr
/// @param show_smpte Show SMPTE drop frame option /// @param show_smpte Show SMPTE drop frame option
agi::vfr::Framerate AskForFPS(bool allow_vfr, bool show_smpte) const; static agi::vfr::Framerate AskForFPS(bool allow_vfr, bool show_smpte);
public:
/// Constructor /// Constructor
/// @param Subtitle format name /// @param Subtitle format name
/// @note Automatically registers the format /// @note Automatically registers the format

View file

@ -0,0 +1,677 @@
// Copyright (c) 2011 Niels Martin Hansen <nielsm@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Aegisub Project http://www.aegisub.org/
/// @file subtitle_format_ebu3264.cpp
/// @see subtitle_format_ebu3264.h
/// @ingroup subtitle_io
// This implements support for the EBU tech 3264 (1991) subtitling data exchange format.
// Work on support for this format was sponsored by Bandai.
#include "config.h"
#include "subtitle_format_ebu3264.h"
#ifndef AGI_PRE
#include <wx/regex.h>
#endif
#include <libaegisub/charset_conv.h>
#include <libaegisub/exception.h>
#include <libaegisub/io.h>
#include <libaegisub/line_wrap.h>
#include <libaegisub/scoped_ptr.h>
#include "aegisub_endian.h"
#include "ass_dialogue.h"
#include "ass_file.h"
#include "ass_override.h"
#include "ass_style.h"
#include "compat.h"
#include "dialog_export_ebu3264.h"
#include "main.h"
#include "text_file_writer.h"
namespace
{
#pragma pack(push, 1)
/// General Subtitle Information block as it appears in the file
struct BlockGSI
{
char cpn[3]; ///< code page number
char dfc[8]; ///< disk format code
char dsc; ///< display standard code
char cct[2]; ///< character code table number
char lc[2]; ///< language code
char opt[32]; ///< original programme title
char oet[32]; ///< original episode title
char tpt[32]; ///< translated programme title
char tet[32]; ///< translated episode title
char tn[32]; ///< translator name
char tcd[32]; ///< translator contact details
char slr[16]; ///< subtitle list reference code
char cd[6]; ///< creation date
char rd[6]; ///< revision date
char rn[2]; ///< revision number
char tnb[5]; ///< total number of TTI blocks
char tns[5]; ///< total number of subtitles
char tng[3]; ///< total number of subtitle groups
char mnc[2]; ///< maximum number of displayable characters in a row
char mnr[2]; ///< maximum number of displayable rows
char tcs; ///< time code: status
char tcp[8]; ///< time code: start of programme
char tcf[8]; ///< time code: first in-cue
char tnd; ///< total number of disks
char dsn; ///< disk sequence number
char co[3]; ///< country of origin
char pub[32]; ///< publisher
char en[32]; ///< editor's name
char ecd[32]; ///< editor's contact details
char unused[75];
char uda[576]; ///< user defined area
};
/// Text and Timing Information block as it appears in the file
struct BlockTTI
{
uint8_t sgn; ///< subtitle group number
uint16_t sn; ///< subtitle number
uint8_t ebn; ///< extension block number
uint8_t cs; ///< cumulative status
EbuTimecode tci; ///< time code in
EbuTimecode tco; ///< time code out
uint8_t vp; ///< vertical position
uint8_t jc; ///< justification code
uint8_t cf; ///< comment flag
char tf[112]; ///< text field
};
#pragma pack(pop)
/// A block of text with basic formatting information
struct EbuFormattedText
{
wxString text; ///< Text in this block
bool underline; ///< Is this block underlined?
bool italic; ///< Is this block italic?
bool word_start; ///< Is it safe to line-wrap between this block and the previous one?
EbuFormattedText(wxString const& t, bool u = false, bool i = false, bool ws = true) : text(t), underline(u), italic(i), word_start(ws) { }
};
typedef std::vector<EbuFormattedText> EbuTextRow;
/// Formatting character constants
const char EBU_FORMAT_ITALIC[] = "\x81\x80";
const char EBU_FORMAT_UNDERLINE[] = "\x83\x82";
const char EBU_FORMAT_BOXING[] = "\x85\x84";
const char EBU_FORMAT_LINEBREAK = '\x8a';
const char EBU_FORMAT_UNUSED_SPACE = '\x8f';
/// intermediate format
class EbuSubtitle
{
void ProcessOverrides(AssDialogueBlockOverride *ob, bool &underline, bool &italic, int &align, bool style_underline, bool style_italic)
{
for (std::vector<AssOverrideTag*>::iterator tag = ob->Tags.begin(); tag != ob->Tags.end(); ++tag)
{
AssOverrideTag *t = *tag;
if (t->Name == "\\u")
underline = t->Params[0]->Get<bool>(style_underline);
else if (t->Name == "\\i")
italic = t->Params[0]->Get<bool>(style_italic);
else if (t->Name == "\\an")
align = t->Params[0]->Get<int>(align);
else if (t->Name == "\\a" && !t->Params[0]->omitted)
align = AssStyle::SsaToAss(t->Params[0]->Get<int>());
}
}
void SetAlignment(int ass_alignment)
{
if (ass_alignment < 1 || ass_alignment > 9)
ass_alignment = 2;
vertical_position = static_cast<VerticalPosition>(ass_alignment / 3);
justification_code = static_cast<JustificationCode>((ass_alignment - 1) % 3 + 1);
}
public:
enum CumulativeStatus
{
NotCumulative = 0,
CumulativeStart = 1,
CulumativeMiddle = 2,
CumulativeEnd = 3
};
enum JustificationCode
{
UnchangedPresentation = 0,
JustifyLeft = 1,
JustifyCentre = 2,
JustifyRight = 3
};
// note: not set to constants from spec
enum VerticalPosition
{
PositionTop = 2,
PositionMiddle = 1,
PositionBottom = 0
};
int group_number; ///< always 0 for compat
/// subtitle number is assigned when generating blocks
CumulativeStatus cumulative_status; ///< always NotCumulative for compat
int time_in; ///< frame number
int time_out; ///< frame number
bool comment_flag; ///< always false for compat
JustificationCode justification_code; ///< never Unchanged presentation for compat
VerticalPosition vertical_position; ///< translated to row on tti conversion
std::vector<EbuTextRow> text_rows; ///< text split into rows, still unicode
EbuSubtitle()
: group_number(0)
, cumulative_status(NotCumulative)
, time_in(0)
, time_out(0)
, comment_flag(false)
, justification_code(JustifyCentre)
, vertical_position(PositionBottom)
, text_rows()
{
}
void SplitLines(int max_width, int split_type)
{
// split_type is an SSA wrap style number
if (split_type == 2) return; // no wrapping here!
if (split_type < 0) return;
if (split_type > 4) return;
std::vector<EbuTextRow> new_text;
new_text.reserve(text_rows.size());
for (std::vector<EbuTextRow>::iterator row = text_rows.begin(); row != text_rows.end(); ++row)
{
// Get lengths of each word
std::vector<size_t> word_lengths;
for (EbuTextRow::iterator cur_block = row->begin(); cur_block != row->end(); ++cur_block)
{
if (cur_block->word_start)
word_lengths.push_back(0);
word_lengths.back() += cur_block->text.size();
}
std::vector<size_t> split_points = agi::get_wrap_points(word_lengths, (size_t)max_width, (agi::WrapMode)split_type);
if (split_points.empty())
{
// Line doesn't need splitting, so copy straight over
new_text.push_back(*row);
continue;
}
// Apply the splits
new_text.push_back(EbuTextRow());
size_t cur_word = 0;
size_t split_point = 0;
for (EbuTextRow::iterator cur_block = row->begin(); cur_block != row->end(); ++cur_block)
{
if (cur_block->word_start && split_point < split_points.size())
{
if (split_points[split_point] == cur_word)
{
new_text.push_back(EbuTextRow());
++split_point;
}
++cur_word;
}
new_text.back().push_back(*cur_block);
}
}
// replace old text
swap(text_rows, new_text);
}
bool CheckLineLengths(int max_width) const
{
for (std::vector<EbuTextRow>::const_iterator row = text_rows.begin(); row != text_rows.end(); ++row)
{
int line_length = 0;
for (EbuTextRow::const_iterator it = row->begin(); it != row->end(); ++it)
line_length += it->text.size();
if (line_length > max_width)
// early return as soon as any line is over length
return false;
}
// no lines failed
return true;
}
void SetTextFromAss(AssDialogue *line, bool style_underline, bool style_italic, int align, int wrap_mode)
{
// Helper for finding special characters
wxRegEx special_char_search("\\\\[nN]| ", wxRE_ADVANCED);
line->ParseASSTags();
text_rows.clear();
text_rows.push_back(EbuTextRow());
// current row being worked on
EbuTextRow *cur_row = &text_rows.back();
// create initial text part
cur_row->push_back(EbuFormattedText("", style_underline, style_italic, true));
bool underline = style_underline, italic = style_italic;
for (std::vector<AssDialogueBlock*>::iterator bl = line->Blocks.begin(); bl != line->Blocks.end(); ++bl)
{
AssDialogueBlock *b = *bl;
switch (b->GetType())
{
case BLOCK_PLAIN:
// find special characters and convert them
{
wxString text = b->GetText();
// Skip comments
if (text.size() > 1 && text[0] =='{' && text.Last() == '}')
continue;
text.Replace("\\t", " ");
while (special_char_search.Matches(text))
{
size_t start, len;
special_char_search.GetMatch(&start, &len);
// add first part of text to current part
cur_row->back().text.append(text.Left(start));
// process special character
wxString substr = text.Mid(start, len);
if (substr == "\\N" || (wrap_mode == 1 && substr == "\\n"))
{
// create a new row with current style
text_rows.push_back(EbuTextRow());
cur_row = &text_rows.back();
cur_row->push_back(EbuFormattedText("", underline, italic, true));
}
else // if (substr == " " || substr == "\\h" || substr == "\\n")
{
cur_row->back().text.append(" ");
cur_row->push_back(EbuFormattedText("", underline, italic, true));
}
text = text.Mid(start+len);
}
// add the remaining text
cur_row->back().text.append(text);
// convert \h to regular spaces
// done after parsing so that words aren't split on \h
cur_row->back().text.Replace("\\h", " ");
}
break;
case BLOCK_OVERRIDE:
// find relevant tags and process them
{
AssDialogueBlockOverride *ob = static_cast<AssDialogueBlockOverride*>(b);
ob->ParseTags();
ProcessOverrides(ob, underline, italic, align, style_underline, style_italic);
// apply any changes
if (underline != cur_row->back().underline || italic != cur_row->back().italic)
{
if (!cur_row->back().text)
{
// current part is empty, we can safely change formatting on it
cur_row->back().underline = underline;
cur_row->back().italic = italic;
}
else
{
// create a new empty part with new style
cur_row->push_back(EbuFormattedText("", underline, italic, false));
}
}
}
break;
default:
// ignore block, we don't want to output it (drawing or unknown)
break;
}
}
line->ClearBlocks();
SetAlignment(align);
}
};
std::vector<EbuSubtitle> convert_subtitles(AssFile &copy, EbuExportSettings const& export_settings)
{
SubtitleFormat::StripComments(copy.Line);
copy.Sort();
SubtitleFormat::RecombineOverlaps(copy.Line);
SubtitleFormat::MergeIdentical(copy.Line);
int line_wrap_type = copy.GetScriptInfoAsInt("WrapStyle");
agi::vfr::Framerate fps = export_settings.GetFramerate();
EbuTimecode tcofs = export_settings.timecode_offset;
int timecode_bias = fps.FrameAtSmpte(tcofs.h, tcofs.m, tcofs.s, tcofs.s);
AssStyle default_style;
std::vector<EbuSubtitle> subs_list;
subs_list.reserve(copy.Line.size());
// convert to intermediate format
for (entryIter orgline = copy.Line.begin(); orgline != copy.Line.end(); ++orgline)
{
AssDialogue *line = dynamic_cast<AssDialogue*>(*orgline);
if (!line) continue;
// add a new subtitle and work on it
subs_list.push_back(EbuSubtitle());
EbuSubtitle &imline = subs_list.back();
// some defaults for compatibility
imline.group_number = 0;
imline.comment_flag = false;
imline.cumulative_status = EbuSubtitle::NotCumulative;
// convert times
imline.time_in = fps.FrameAtTime(line->Start) + timecode_bias;
imline.time_out = fps.FrameAtTime(line->End) + timecode_bias;
if (export_settings.inclusive_end_times)
// cheap and possibly wrong way to ensure exclusive times, subtract one frame from end time
imline.time_out -= 1;
// convert alignment from style
AssStyle *style = copy.GetStyle(line->Style);
if (!style)
style = &default_style;
// add text, translate formatting
imline.SetTextFromAss(line, style->underline, style->italic, style->alignment, line_wrap_type);
// line breaking handling
if (export_settings.line_wrapping_mode == EbuExportSettings::AutoWrap)
imline.SplitLines(export_settings.max_line_length, line_wrap_type);
else if (export_settings.line_wrapping_mode == EbuExportSettings::AutoWrapBalance)
imline.SplitLines(export_settings.max_line_length, agi::Wrap_Balanced);
else if (!imline.CheckLineLengths(export_settings.max_line_length))
{
if (export_settings.line_wrapping_mode == EbuExportSettings::AbortOverLength)
throw Ebu3264SubtitleFormat::ConversionFailed(STD_STR(wxString::Format(_("Line over maximum length: %s"), line->Text.c_str())), 0);
else // skip over-long lines
subs_list.pop_back();
}
}
// produce an empty line if there are none
// (it still has to contain a space to not get ignored)
if (subs_list.empty())
{
subs_list.push_back(EbuSubtitle());
subs_list.back().text_rows.push_back(EbuTextRow());
subs_list.back().text_rows.back().push_back(EbuFormattedText(" "));
}
return subs_list;
}
inline size_t buffer_size(wxString const& str)
{
#if wxUSE_UNICODE_UTF8
return str.utf8_length();
#else
return str.length() * sizeof(wxStringCharType);
#endif
}
inline const char *wx_str(wxString const& str)
{
return reinterpret_cast<const char *>(str.wx_str());
}
std::string convert_subtitle_line(std::vector<EbuSubtitle>::const_iterator sub, agi::charset::IconvWrapper *encoder)
{
std::string fullstring;
for (std::vector<EbuTextRow>::const_iterator row = sub->text_rows.begin(); row != sub->text_rows.end(); ++row)
{
// formatting is reset at the start of every row, so keep track per row
bool underline = false, italic = false;
for (std::vector<EbuFormattedText>::const_iterator block = row->begin(); block != row->end(); ++block)
{
// insert codes for changed formatting
if (underline != block->underline)
fullstring += EBU_FORMAT_UNDERLINE[block->underline];
if (italic != block->italic)
fullstring += EBU_FORMAT_ITALIC[block->italic];
underline = block->underline;
italic = block->italic;
// convert text to specified encoding
fullstring += encoder->Convert(std::string(wx_str(block->text), buffer_size(block->text)));
}
// check that this is not the last row
if (row+1 != sub->text_rows.end())
// insert linebreak for non-final lines
fullstring += EBU_FORMAT_LINEBREAK;
}
return fullstring;
}
void smpte_at_frame(agi::vfr::Framerate const& fps, int frame, EbuTimecode &tc)
{
int h=0, m=0, s=0, f=0;
fps.SmpteAtFrame(frame, &h, &m, &s, &f);
tc.h = h;
tc.m = m;
tc.s = s;
tc.f = f;
}
std::vector<BlockTTI> create_blocks(std::vector<EbuSubtitle> const& subs_list, EbuExportSettings const& export_settings)
{
agi::scoped_ptr<agi::charset::IconvWrapper> encoder(export_settings.GetTextEncoder());
agi::vfr::Framerate fps = export_settings.GetFramerate();
uint16_t subtitle_number = 0;
std::vector<BlockTTI> tti;
tti.reserve(subs_list.size());
for (std::vector<EbuSubtitle>::const_iterator sub = subs_list.begin(); sub != subs_list.end(); ++sub)
{
std::string fullstring = convert_subtitle_line(sub, encoder.get());
// construct a base block that can be copied and filled
BlockTTI base;
base.sgn = sub->group_number;
base.sn = Endian::MachineToLittle(subtitle_number++);
base.ebn = 255;
base.cf = sub->comment_flag;
memset(base.tf, EBU_FORMAT_UNUSED_SPACE, sizeof(base.tf));
smpte_at_frame(fps, sub->time_in, base.tci);
smpte_at_frame(fps, sub->time_out, base.tco);
base.cs = sub->cumulative_status;
if (export_settings.translate_alignments)
{
// vertical position
if (sub->vertical_position == EbuSubtitle::PositionTop)
base.vp = 0;
else if (sub->vertical_position == EbuSubtitle::PositionMiddle)
base.vp = std::min<size_t>(0, 50 - (10 * sub->text_rows.size()));
else //if (sub->vertical_position == EbuSubtitle::PositionBottom)
base.vp = 99;
base.jc = sub->justification_code;
}
else
{
base.vp = 99;
base.jc = EbuSubtitle::JustifyCentre;
}
// produce blocks from string
static const size_t block_size = sizeof(((BlockTTI*)0)->tf);
uint8_t num_blocks = 0;
for (size_t pos = 0; pos < fullstring.size(); pos += block_size)
{
size_t bytes_remaining = fullstring.size() - pos;
tti.push_back(base);
// write an extension block number if the remaining text doesn't fit in the block
tti.back().ebn = bytes_remaining > block_size ? num_blocks++ : 255;
std::copy(&fullstring[pos], &fullstring[pos + std::min(block_size, bytes_remaining)], tti.back().tf);
}
}
return tti;
}
#ifdef _MSC_VER
#define snprintf _snprintf
#endif
BlockGSI create_header(AssFile const& copy, EbuExportSettings const& export_settings)
{
wxString scriptinfo_title = copy.GetScriptInfo("Title");
wxString scriptinfo_translation = copy.GetScriptInfo("Original Translation");
wxString scriptinfo_editing = copy.GetScriptInfo("Original Editing");
agi::charset::IconvWrapper gsi_encoder(wxSTRING_ENCODING, "CP850");
BlockGSI gsi;
memset(&gsi, 0x20, sizeof(gsi)); // fill with spaces
memcpy(gsi.cpn, "850", 3);
switch (export_settings.tv_standard)
{
case EbuExportSettings::STL23:
case EbuExportSettings::STL24:
memcpy(gsi.dfc, "STL24.01", 8);
break;
case EbuExportSettings::STL29:
case EbuExportSettings::STL29drop:
case EbuExportSettings::STL30:
memcpy(gsi.dfc, "STL30.01", 8);
break;
case EbuExportSettings::STL25:
default:
memcpy(gsi.dfc, "STL25.01", 8);
break;
}
gsi.dsc = '0'; // open subtitling
gsi.cct[0] = '0';
gsi.cct[1] = '0' + (int)export_settings.text_encoding;
if (export_settings.text_encoding == EbuExportSettings::utf8)
memcpy(gsi.cct, "U8", 2);
memcpy(gsi.lc, "00", 2);
gsi_encoder.Convert(wx_str(scriptinfo_title), buffer_size(scriptinfo_title), gsi.opt, 32);
gsi_encoder.Convert(wx_str(scriptinfo_translation), buffer_size(scriptinfo_translation), gsi.tn, 32);
{
char buf[20];
time_t now;
time(&now);
tm *thetime = localtime(&now);
strftime(buf, 20, "AGI-%y%m%d%H%M%S", thetime);
memcpy(gsi.slr, buf, 16);
strftime(buf, 20, "%y%m%d", thetime);
memcpy(gsi.cd, buf, 6);
memcpy(gsi.rd, buf, 6);
memcpy(gsi.rn, "00", 2);
memcpy(gsi.tng, "001", 3);
snprintf(gsi.mnc, 2, "%02u", (unsigned int)export_settings.max_line_length);
memcpy(gsi.mnr, "99", 2);
gsi.tcs = '1';
EbuTimecode tcofs = export_settings.timecode_offset;
snprintf(gsi.tcp, 8, "%02u%02u%02u%02u", (unsigned int)tcofs.h, (unsigned int)tcofs.m, (unsigned int)tcofs.s, (unsigned int)tcofs.s);
}
gsi.tnd = '1';
gsi.dsn = '1';
memcpy(gsi.co, "NTZ", 3); // neutral zone!
gsi_encoder.Convert(wx_str(scriptinfo_editing), buffer_size(scriptinfo_editing), gsi.en, 32);
if (export_settings.text_encoding == EbuExportSettings::utf8)
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));
return gsi;
}
EbuExportSettings get_export_config(wxWindow *parent)
{
EbuExportSettings s("Subtitle Format/EBU STL");
// Disable the busy cursor set by the exporter while the dialog is visible
wxEndBusyCursor();
int res = EbuExportConfigurationDialog(parent, s).ShowModal();
wxBeginBusyCursor();
if (res != wxID_OK)
throw agi::UserCancelException("EBU/STL export");
s.Save();
return s;
}
} // namespace {
Ebu3264SubtitleFormat::Ebu3264SubtitleFormat()
: SubtitleFormat("EBU subtitling data exchange format (EBU tech 3264, 1991)")
{
}
wxArrayString Ebu3264SubtitleFormat::GetWriteWildcards() const
{
wxArrayString formats;
formats.Add("stl");
return formats;
}
void Ebu3264SubtitleFormat::WriteFile(const AssFile *src, wxString const& filename, wxString const& encoding) const
{
// collect data from user
EbuExportSettings export_settings = get_export_config(0);
AssFile copy(*src);
std::vector<EbuSubtitle> subs_list = convert_subtitles(copy, export_settings);
std::vector<BlockTTI> tti = create_blocks(subs_list, export_settings);
BlockGSI gsi = create_header(copy, export_settings);
BlockTTI &block0 = tti.front();
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);
snprintf(gsi.tnb, 5, "%5u", (unsigned int)tti.size());
snprintf(gsi.tns, 5, "%5u", (unsigned int)subs_list.size());
// write file
agi::io::Save f(STD_STR(filename), true);
f.Get().write((const char *)&gsi, sizeof(gsi));
for (std::vector<BlockTTI>::iterator block = tti.begin(); block != tti.end(); ++block)
{
f.Get().write((const char *)&*block, sizeof(*block));
}
}

View file

@ -0,0 +1,34 @@
// Copyright (c) 2011 Niels Martin Hansen <nielsm@aegisub.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
//
// Aegisub Project http://www.aegisub.org/
/// @file subtitle_format_ebu3264.h
/// @see subtitle_format_ebu3264.cpp
/// @ingroup subtitle_io
#include "subtitle_format.h"
/// @brief Subtitle writer for the EBU tech 3264 (1991) subtitling data exchange format
///
/// Based on specifications obtained at <http://tech.ebu.ch/docs/tech/tech3264.pdf>
/// Work on support for this format was sponsored by Bandai.
class Ebu3264SubtitleFormat : public SubtitleFormat {
public:
Ebu3264SubtitleFormat();
wxArrayString GetWriteWildcards() const;
void WriteFile(const AssFile *src, wxString const& filename, wxString const& encoding) const;
DEFINE_SIMPLE_EXCEPTION(ConversionFailed, agi::InvalidInputException, "subtitle_io/ebu3264/conversion_error")
};