From 4294e5857d989c4ff5d08ace3eb63e1cf5c52fe3 Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Thu, 29 Mar 2012 19:05:26 +0000 Subject: [PATCH] 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. --- .../aegisub_vs2008/aegisub_vs2008.vcproj | 16 + aegisub/build/msbuild/Aegisub/Aegisub.vcxproj | 4 + .../msbuild/Aegisub/Aegisub.vcxproj.filters | 12 + aegisub/src/Makefile | 2 + aegisub/src/agi_pre.h | 1 + aegisub/src/dialog_export_ebu3264.cpp | 258 +++++++ aegisub/src/dialog_export_ebu3264.h | 112 +++ aegisub/src/libresrc/default_config.json | 17 + aegisub/src/subtitle_format.cpp | 16 +- aegisub/src/subtitle_format.h | 17 +- aegisub/src/subtitle_format_ebu3264.cpp | 677 ++++++++++++++++++ aegisub/src/subtitle_format_ebu3264.h | 34 + 12 files changed, 1150 insertions(+), 16 deletions(-) create mode 100644 aegisub/src/dialog_export_ebu3264.cpp create mode 100644 aegisub/src/dialog_export_ebu3264.h create mode 100644 aegisub/src/subtitle_format_ebu3264.cpp create mode 100644 aegisub/src/subtitle_format_ebu3264.h diff --git a/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj b/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj index a8f7a20f1..daf68b8e2 100644 --- a/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj +++ b/aegisub/build/aegisub_vs2008/aegisub_vs2008.vcproj @@ -1035,6 +1035,14 @@ RelativePath="..\..\src\dialog_export.h" > + + + + @@ -1439,6 +1447,14 @@ RelativePath="..\..\src\subtitle_format_ass.h" > + + + + diff --git a/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj b/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj index 11b689c6f..aa7175085 100644 --- a/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj +++ b/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj @@ -109,6 +109,7 @@ + @@ -170,6 +171,7 @@ + @@ -293,6 +295,7 @@ + @@ -360,6 +363,7 @@ + diff --git a/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj.filters b/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj.filters index f1cd9c198..80c12a962 100644 --- a/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj.filters +++ b/aegisub/build/msbuild/Aegisub/Aegisub.vcxproj.filters @@ -297,6 +297,9 @@ Features\Export + + Features\Export + Features\Export @@ -351,6 +354,9 @@ Subtitle formats + + Subtitle formats + Subtitle formats @@ -788,6 +794,9 @@ Subtitle formats + + Subtitle formats + Subtitle formats @@ -881,6 +890,9 @@ Features\Export + + Features\Export + Features\Export diff --git a/aegisub/src/Makefile b/aegisub/src/Makefile index 820f8c70d..9be3d5163 100644 --- a/aegisub/src/Makefile +++ b/aegisub/src/Makefile @@ -166,6 +166,7 @@ SRC += \ dialog_detached_video.cpp \ dialog_dummy_video.cpp \ dialog_export.cpp \ + dialog_export_ebu3264.cpp \ dialog_fonts_collector.cpp \ dialog_jumpto.cpp \ dialog_kara_timing_copy.cpp \ @@ -221,6 +222,7 @@ SRC += \ subs_preview.cpp \ subtitle_format.cpp \ subtitle_format_ass.cpp \ + subtitle_format_ebu3264.cpp \ subtitle_format_encore.cpp \ subtitle_format_microdvd.cpp \ subtitle_format_mkv.cpp \ diff --git a/aegisub/src/agi_pre.h b/aegisub/src/agi_pre.h index ac38444be..b2664a7a4 100644 --- a/aegisub/src/agi_pre.h +++ b/aegisub/src/agi_pre.h @@ -193,6 +193,7 @@ #include #include #include +#include #include #include #include diff --git a/aegisub/src/dialog_export_ebu3264.cpp b/aegisub/src/dialog_export_ebu3264.cpp new file mode 100644 index 000000000..d491360d9 --- /dev/null +++ b/aegisub/src/dialog_export_ebu3264.cpp @@ -0,0 +1,258 @@ +// Copyright (c) 2011 Niels Martin Hansen +// Copyright (c) 2012 Thomas Goyne +// +// 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 + +#include "main.h" +#include "text_file_writer.h" + +#ifndef AGI_PRE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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(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); +} diff --git a/aegisub/src/dialog_export_ebu3264.h b/aegisub/src/dialog_export_ebu3264.h new file mode 100644 index 000000000..18ee2bccb --- /dev/null +++ b/aegisub/src/dialog_export_ebu3264.h @@ -0,0 +1,112 @@ +// Copyright (c) 2011 Niels Martin Hansen +// Copyright (c) 2012 Thomas Goyne +// +// 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 +#endif + +#include + +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); +}; diff --git a/aegisub/src/libresrc/default_config.json b/aegisub/src/libresrc/default_config.json index 4c81c2c75..259627e6f 100644 --- a/aegisub/src/libresrc/default_config.json +++ b/aegisub/src/libresrc/default_config.json @@ -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" : { "Default Duration" : 2000 }, diff --git a/aegisub/src/subtitle_format.cpp b/aegisub/src/subtitle_format.cpp index f4758250a..ec59bfa53 100644 --- a/aegisub/src/subtitle_format.cpp +++ b/aegisub/src/subtitle_format.cpp @@ -48,6 +48,7 @@ #include "ass_file.h" #include "ass_style.h" #include "subtitle_format_ass.h" +#include "subtitle_format_ebu3264.h" #include "subtitle_format_encore.h" #include "subtitle_format_microdvd.h" #include "subtitle_format_mkv.h" @@ -100,7 +101,7 @@ bool SubtitleFormat::CanSave(const AssFile *subs) const { 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; // Video FPS @@ -164,7 +165,7 @@ agi::vfr::Framerate SubtitleFormat::AskForFPS(bool allow_vfr, bool show_smpte) c return Framerate(); } -void SubtitleFormat::StripTags(LineList &lines) const { +void SubtitleFormat::StripTags(LineList &lines) { for (LineList::iterator cur = lines.begin(); cur != lines.end(); ++cur) { if (AssDialogue *current = dynamic_cast(*cur)) { 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) { if (AssDialogue *current = dynamic_cast(*cur)) { 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(); ) { AssDialogue *diag = dynamic_cast(*it); 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(); ) { if (dynamic_cast(*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 /// /// 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(); cur = next++; @@ -288,7 +289,7 @@ void SubtitleFormat::RecombineOverlaps(LineList &lines) const { } /// @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(); cur = next++; @@ -313,6 +314,7 @@ std::list SubtitleFormat::formats; void SubtitleFormat::LoadFormats() { if (formats.empty()) { new ASSSubtitleFormat; + new Ebu3264SubtitleFormat; new EncoreSubtitleFormat; new MKVSubtitleFormat; new MicroDVDSubtitleFormat; diff --git a/aegisub/src/subtitle_format.h b/aegisub/src/subtitle_format.h index a43f59a63..6e94a3330 100644 --- a/aegisub/src/subtitle_format.h +++ b/aegisub/src/subtitle_format.h @@ -65,32 +65,31 @@ class SubtitleFormat { /// List of loaded subtitle formats static std::list formats; -protected: +public: typedef std::list LineList; /// Strip override tags - void StripTags(LineList &lines) const; + static void StripTags(LineList &lines); /// Convert newlines to the specified character(s) /// @param lineEnd newline character(s) /// @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 - void StripComments(LineList &lines) const; + static void StripComments(LineList &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 /// /// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge - void RecombineOverlaps(LineList &lines) const; + static void RecombineOverlaps(LineList &lines); /// Merge sequential identical lines - void MergeIdentical(LineList &lines) const; + static void MergeIdentical(LineList &lines); /// 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 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 /// @param Subtitle format name /// @note Automatically registers the format diff --git a/aegisub/src/subtitle_format_ebu3264.cpp b/aegisub/src/subtitle_format_ebu3264.cpp new file mode 100644 index 000000000..e3034cf27 --- /dev/null +++ b/aegisub/src/subtitle_format_ebu3264.cpp @@ -0,0 +1,677 @@ +// Copyright (c) 2011 Niels Martin Hansen +// +// 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 +#endif + +#include +#include +#include +#include +#include + +#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 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::iterator tag = ob->Tags.begin(); tag != ob->Tags.end(); ++tag) + { + AssOverrideTag *t = *tag; + if (t->Name == "\\u") + underline = t->Params[0]->Get(style_underline); + else if (t->Name == "\\i") + italic = t->Params[0]->Get(style_italic); + else if (t->Name == "\\an") + align = t->Params[0]->Get(align); + else if (t->Name == "\\a" && !t->Params[0]->omitted) + align = AssStyle::SsaToAss(t->Params[0]->Get()); + } + } + + void SetAlignment(int ass_alignment) + { + if (ass_alignment < 1 || ass_alignment > 9) + ass_alignment = 2; + + vertical_position = static_cast(ass_alignment / 3); + justification_code = static_cast((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 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 new_text; + new_text.reserve(text_rows.size()); + + for (std::vector::iterator row = text_rows.begin(); row != text_rows.end(); ++row) + { + // Get lengths of each word + std::vector 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 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::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::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(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 convert_subtitles(AssFile ©, 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 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(*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(str.wx_str()); + } + + std::string convert_subtitle_line(std::vector::const_iterator sub, agi::charset::IconvWrapper *encoder) + { + std::string fullstring; + for (std::vector::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::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 create_blocks(std::vector const& subs_list, EbuExportSettings const& export_settings) + { + agi::scoped_ptr encoder(export_settings.GetTextEncoder()); + agi::vfr::Framerate fps = export_settings.GetFramerate(); + + uint16_t subtitle_number = 0; + + std::vector tti; + tti.reserve(subs_list.size()); + for (std::vector::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(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 subs_list = convert_subtitles(copy, export_settings); + std::vector 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::iterator block = tti.begin(); block != tti.end(); ++block) + { + f.Get().write((const char *)&*block, sizeof(*block)); + } +} diff --git a/aegisub/src/subtitle_format_ebu3264.h b/aegisub/src/subtitle_format_ebu3264.h new file mode 100644 index 000000000..b76d472c0 --- /dev/null +++ b/aegisub/src/subtitle_format_ebu3264.h @@ -0,0 +1,34 @@ +// Copyright (c) 2011 Niels Martin Hansen +// +// 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 +/// 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") +};