diff --git a/aegisub/auto4_auto3.cpp b/aegisub/auto4_auto3.cpp index 54f1577c9..2f410205e 100644 --- a/aegisub/auto4_auto3.cpp +++ b/aegisub/auto4_auto3.cpp @@ -35,9 +35,107 @@ #include "auto4_auto3.h" #include "auto4_lua.h" +#include "../lua51/src/lualib.h" +#include "../lua51/src/lauxlib.h" +#include "options.h" +#include "string_codec.h" +#include "vfr.h" +#include "ass_override.h" namespace Automation4 { + // Helper functions for reading/writing data + + static inline void L_settable(lua_State *L, int table, const char *key, lua_Number val) + { + lua_pushstring(L, key); + lua_pushnumber(L, val); + if (table > 0 || table < -100) { + lua_settable(L, table); + } else { + lua_settable(L, table-2); + } + } + + static inline void L_settable(lua_State *L, int table, const char *key, wxString val) + { + //wxLogMessage(_T("Adding string at index '%s': %s"), wxString(key, wxConvUTF8), val); + lua_pushstring(L, key); + lua_pushstring(L, val.mb_str(wxConvUTF8)); + if (table > 0 || table < -100) { + lua_settable(L, table); + } else { + lua_settable(L, table-2); + } + } + + static inline void L_settable_bool(lua_State *L, int table, const char *key, bool val) + { + lua_pushstring(L, key); + lua_pushboolean(L, val?1:0); + if (table > 0 || table < -100) { + lua_settable(L, table); + } else { + lua_settable(L, table-2); + } + } + + static inline void L_settable(lua_State *L, int table, wxString &key, lua_Number val) + { + L_settable(L, table, key.mb_str(wxConvUTF8), val); + } + + static inline void L_settable(lua_State *L, int table, wxString &key, wxString val) + { + L_settable(L, table, key.mb_str(wxConvUTF8), val); + } + + static inline void L_settable_bool(lua_State *L, int table, wxString &key, bool val) + { + L_settable_bool(L, table, key.mb_str(wxConvUTF8).data(), val); + } + + static inline void L_settable_kara(lua_State *L, int table, int index, int duration, wxString &kind, wxString &text, wxString &text_stripped) + { + lua_newtable(L); + L_settable(L, -1, "duration", duration); + L_settable(L, -1, "kind", kind); + L_settable(L, -1, "text", text); + L_settable(L, -1, "text_stripped", text_stripped); + if (table > 0 || table < -100) { + lua_rawseti(L, table, index); + } else { + lua_rawseti(L, table-1, index); + } + } + + static inline lua_Number L_gettableN(lua_State *L, const char *key) + { + lua_pushstring(L, key); + lua_gettable(L, -2); + lua_Number res = lua_tonumber(L, -1); + lua_settop(L, -2); + return res; + } + + static inline wxString L_gettableS(lua_State *L, const char *key) + { + lua_pushstring(L, key); + lua_gettable(L, -2); + wxString res(lua_tostring(L, -1), wxConvUTF8); + lua_settop(L, -2); + return res; + } + + static inline bool L_gettableB(lua_State *L, const char *key) + { + lua_pushstring(L, key); + lua_gettable(L, -2); + bool res = lua_toboolean(L, -1) != 0; + lua_settop(L, -2); + return res; + } + // Auto3ProgressSink @@ -54,6 +152,7 @@ namespace Automation4 { Auto3ProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1)); wxString msg(lua_tostring(L, 1), wxConvUTF8); ps->AddDebugOutput(msg); + ps->AddDebugOutput(_T("\n")); return 0; } @@ -122,29 +221,429 @@ namespace Automation4 { wxWindow* Auto3ConfigDialog::CreateWindow(wxWindow *parent) { - // TODO - return 0; + if (options.size() == 0) + return 0; + + wxPanel *res = new wxPanel(parent, -1); + + wxFlexGridSizer *sizer = new wxFlexGridSizer(2, 5, 5); + + for (std::vector::iterator opt = options.begin(); opt != options.end(); opt++) { + if (opt->kind == COK_INVALID) + continue; + + Control control; + control.option = &*opt; + + switch (opt->kind) { + case COK_LABEL: + control.control = new wxStaticText(res, -1, opt->label); + break; + + case COK_TEXT: + control.control = new wxTextCtrl(res, -1, opt->value.stringval); + break; + + case COK_INT: + control.control = new wxSpinCtrl(res, -1); + if (opt->min.isset && opt->max.isset) { + ((wxSpinCtrl*)control.control)->SetRange(opt->min.intval, opt->max.intval); + } else if (opt->min.isset) { + ((wxSpinCtrl*)control.control)->SetRange(opt->min.intval, 0x7fff); + } else if (opt->max.isset) { + ((wxSpinCtrl*)control.control)->SetRange(-0x7fff, opt->max.intval); + } else { + ((wxSpinCtrl*)control.control)->SetRange(-0x7fff, 0x7fff); + } + ((wxSpinCtrl*)control.control)->SetValue(opt->value.intval); + break; + + case COK_FLOAT: + control.control = new wxTextCtrl(res, -1, wxString::Format(_T("%f"), opt->value.floatval)); + break; + + case COK_BOOL: + control.control = new wxCheckBox(res, -1, opt->label); + ((wxCheckBox*)control.control)->SetValue(opt->value.boolval); + break; + + case COK_COLOUR: + // *FIXME* what to do here? + // just put a stupid edit box for now + control.control = new wxTextCtrl(res, -1, opt->value.colourval.GetASSFormatted(false)); + break; + + case COK_STYLE: + control.control = new wxChoice(res, -1, wxDefaultPosition, wxDefaultSize, AssFile::top->GetStyles()); + ((wxChoice*)control.control)->Insert(_T(""), 0); + break; + + } + + if (opt->kind != COK_LABEL && opt->kind != COK_BOOL) { + control.label = new wxStaticText(res, -1, opt->label); + sizer->Add(control.label, 0, wxALIGN_LEFT|wxALIGN_CENTER_VERTICAL); + } else { + control.label = 0; + sizer->AddSpacer(0); + } + control.control->SetToolTip(opt->hint); + sizer->Add(control.control, 1, wxEXPAND); + + controls.push_back(control); + } + + res->SetSizerAndFit(sizer); + + return res; } - Auto3ConfigDialog::Auto3ConfigDialog(lua_State *_L, bool include_buttons) + Auto3ConfigDialog::Auto3ConfigDialog(lua_State *L, const wxString &_ident) + : ident(_ident) { - // TODO + present = false; + if (!lua_istable(L, -1)) { + return; + } + + int i = 1; + while (true) { + // get an element from the array + lua_pushnumber(L, i); + lua_gettable(L, -2); + + // check if it was a table + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + break; + } + + // add a new config option and fill it + { + Auto3ScriptConfigurationOption opt; + options.push_back(opt); + } + Auto3ScriptConfigurationOption &opt = options.back(); + + // get the "kind" + lua_pushstring(L, "kind"); + lua_gettable(L, -2); + if (lua_isstring(L, -1)) { + // use C standard lib functions here, as it's probably faster than messing around with unicode + // lua is known to always properly null-terminate strings, and the strings are known to be pure ascii + const char *kind = lua_tostring(L, -1); + if (strcmp(kind, "label") == 0) { + opt.kind = COK_LABEL; + } else if (strcmp(kind, "text") == 0) { + opt.kind = COK_TEXT; + } else if (strcmp(kind, "int") == 0) { + opt.kind = COK_INT; + } else if (strcmp(kind, "float") == 0) { + opt.kind = COK_FLOAT; + } else if (strcmp(kind, "bool") == 0) { + opt.kind = COK_BOOL; + } else if (strcmp(kind, "colour") == 0) { + opt.kind = COK_COLOUR; + } else if (strcmp(kind, "style") == 0) { + opt.kind = COK_STYLE; + } else { + opt.kind = COK_INVALID; + } + } else { + opt.kind = COK_INVALID; + } + + // remove "kind" string from stack again + lua_pop(L, 1); + + // no need to check for rest if this one is already deemed invalid + if (opt.kind != COK_INVALID) { + // name + lua_pushstring(L, "name"); + lua_gettable(L, -2); + if (lua_isstring(L, -1)) { + opt.name = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + } else { + lua_pop(L, 1); + // no name means invalid option + opt.kind = COK_INVALID; + goto continue_invalid_option; + } + + // label + lua_pushstring(L, "label"); + lua_gettable(L, -2); + if (lua_isstring(L, -1)) { + opt.label = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + } else { + lua_pop(L, 1); + // label is also required + opt.kind = COK_INVALID; + goto continue_invalid_option; + } + assert(opt.kind != COK_INVALID); + + // hint + lua_pushstring(L, "hint"); + lua_gettable(L, -2); + if (lua_isstring(L, -1)) { + opt.hint = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + opt.hint = _T(""); + } + lua_pop(L, 1); + + // min + lua_pushstring(L, "min"); + lua_gettable(L, -2); + if (lua_isnumber(L, -1)) { + opt.min.isset = true; + opt.min.floatval = lua_tonumber(L, -1); + opt.min.intval = (int)opt.min.floatval; + } else { + opt.min.isset = false; + } + lua_pop(L, 1); + + // max + lua_pushstring(L, "max"); + lua_gettable(L, -2); + if (lua_isnumber(L, -1)) { + opt.max.isset = true; + opt.max.floatval = lua_tonumber(L, -1); + opt.max.intval = (int)opt.max.floatval; + } else { + opt.max.isset = false; + } + lua_pop(L, 1); + + // default (this is going to kill me) + lua_pushstring(L, "default"); + lua_gettable(L, -2); + switch (opt.kind) { + case COK_LABEL: + // nothing to do, nothing expected + break; + case COK_TEXT: + case COK_STYLE: + // expect it to be a string + if (lua_isstring(L, -1)) { + opt.default_val.stringval = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + // not a string, baaaad scripter + opt.kind = COK_INVALID; + } + break; + case COK_INT: + case COK_FLOAT: + // expect it to be a number + if (lua_isnumber(L, -1)) { + opt.default_val.floatval = lua_tonumber(L, -1); + opt.default_val.intval = (int)opt.default_val.floatval; + } else { + opt.kind = COK_INVALID; + } + break; + case COK_BOOL: + // expect it to be a bool + if (lua_isboolean(L, -1)) { + opt.default_val.boolval = lua_toboolean(L, -1)!=0; + } else { + opt.kind = COK_INVALID; + } + break; + case COK_COLOUR: + // expect it to be a ass hex colour formatted string + if (lua_isstring(L, -1)) { + opt.default_val.stringval = wxString(lua_tostring(L, -1), wxConvUTF8); + opt.default_val.colourval.Parse(opt.default_val.stringval); // and hope this goes well! + } else { + opt.kind = COK_INVALID; + } + break; + } + opt.value = opt.default_val; + lua_pop(L, 1); + } + + // so we successfully got an option added, so at least there is a configuration present now + present = true; +continue_invalid_option: + // clean up and prepare for next iteration + lua_pop(L, 1); + i++; + } } Auto3ConfigDialog::~Auto3ConfigDialog() { - // TODO + // TODO? } int Auto3ConfigDialog::LuaReadBack(lua_State *L) { - // TODO - return 0; + lua_newtable(L); + + for (std::vector::iterator opt = options.begin(); opt != options.end(); opt++) { + switch (opt->kind) { + case COK_INVALID: + case COK_LABEL: + break; + + case COK_TEXT: + case COK_STYLE: + L_settable(L, -1, opt->name, opt->value.stringval); + break; + + case COK_INT: + L_settable(L, -1, opt->name, opt->value.intval); + break; + + case COK_FLOAT: + L_settable(L, -1, opt->name, opt->value.floatval); + break; + + case COK_BOOL: + L_settable_bool(L, -1, opt->name, opt->value.boolval); + break; + + case COK_COLOUR: + L_settable(L, -1, opt->name, opt->value.colourval.GetASSFormatted(false, false)); + break; + + default: + break; + } + } + + return 1; } void Auto3ConfigDialog::ReadBack() { - // TODO + wxString opthname = wxString::Format(_T("Automation Settings %s"), ident.c_str()); + + for (std::vector::iterator ctl = controls.begin(); ctl != controls.end(); ctl++) { + switch (ctl->option->kind) { + case COK_TEXT: + ctl->option->value.stringval = ((wxTextCtrl*)ctl->control)->GetValue(); + break; + + case COK_INT: + ctl->option->value.intval = ((wxSpinCtrl*)ctl->control)->GetValue(); + break; + + case COK_FLOAT: + if (!((wxTextCtrl*)ctl->control)->GetValue().ToDouble(&ctl->option->value.floatval)) { + wxLogWarning( + _T("The value entered for field '%s' (%s) could not be converted to a floating-point number. Default value (%f) substituted for the entered value."), + ctl->option->label.c_str(), + ((wxTextCtrl*)ctl->control)->GetValue().c_str(), + ctl->option->default_val.floatval); + ctl->option->value.floatval = ctl->option->default_val.floatval; + } + break; + + case COK_BOOL: + ctl->option->value.boolval = ((wxCheckBox*)ctl->control)->GetValue(); + break; + + case COK_COLOUR: + // *FIXME* needs to be updated to use a proper color control + ctl->option->value.colourval.Parse(((wxTextCtrl*)ctl->control)->GetValue()); + break; + + case COK_STYLE: + ctl->option->value.stringval = ((wxChoice*)ctl->control)->GetStringSelection(); + break; + } + } + + // serialize the new settings and save them to the file + AssFile::top->SetScriptInfo(opthname, serialize()); + } + + wxString Auto3ConfigDialog::serialize() + { + if (options.size() == 0) + return _T(""); + + wxString result; + for (std::vector::iterator opt = options.begin(); opt != options.end(); opt++) { + switch (opt->kind) { + case COK_TEXT: + case COK_STYLE: + result << wxString::Format(_T("%s:%s|"), opt->name.c_str(), inline_string_encode(opt->value.stringval).c_str()); + break; + case COK_INT: + result << wxString::Format(_T("%s:%d|"), opt->name.c_str(), opt->value.intval); + break; + case COK_FLOAT: + result << wxString::Format(_T("%s:%e|"), opt->name.c_str(), opt->value.floatval); + break; + case COK_BOOL: + result << wxString::Format(_T("%s:%d|"), opt->name.c_str(), opt->value.boolval?1:0); + break; + case COK_COLOUR: + result << wxString::Format(_T("%s:%s|"), opt->name.c_str(), opt->value.colourval.GetASSFormatted(false).c_str()); + break; + default: + // The rest aren't stored + break; + } + } + if (result.Last() == _T('|')) + result.RemoveLast(); + return result; + } + + void Auto3ConfigDialog::unserialize(wxString &settings) + { + wxStringTokenizer toker(settings, _T("|"), wxTOKEN_STRTOK); + while (toker.HasMoreTokens()) { + // get the parts of this setting + wxString setting = toker.GetNextToken(); + + wxString optname = setting.BeforeFirst(_T(':')); + wxString optval = setting.AfterFirst(_T(':')); + + // find the setting in the list loaded from the script + std::vector::iterator opt = options.begin(); + while (opt != options.end() && opt->name != optname) + opt ++; + + if (opt != options.end()) { + // ok, found the option! + switch (opt->kind) { + case COK_TEXT: + case COK_STYLE: + opt->value.stringval = inline_string_decode(optval); + break; + + case COK_INT: + { + long n; + optval.ToLong(&n, 10); + opt->value.intval = n; + } + break; + + case COK_FLOAT: + optval.ToDouble(&opt->value.floatval); + break; + + case COK_BOOL: + opt->value.boolval = optval == _T("1"); + break; + + case COK_COLOUR: + opt->value.colourval.Parse(optval); + break; + } + } + } } @@ -153,14 +652,24 @@ namespace Automation4 { Auto3Filter::Auto3Filter(const wxString &_name, const wxString &_description, lua_State *_L) : Feature(SCRIPTFEATURE_FILTER, _name) , FeatureFilter(_name, _description, 0) + , L(_L) { - // TODO + // check that the processing function exists + lua_getglobal(L, "process_lines"); + if (!lua_isfunction(L, -1)) { + throw _T("Script error: No 'process_lines' function provided"); + } + + // configuration (let the config object do all the loading) + lua_getglobal(L, "configuration"); + config = new Auto3ConfigDialog(L, GetName()); + + lua_pop(L, 2); } ScriptConfigDialog* Auto3Filter::GenerateConfigDialog(wxWindow *parent) { - // TODO - return 0; + return config; } void Auto3Filter::Init() @@ -170,21 +679,481 @@ namespace Automation4 { void Auto3Filter::ProcessSubs(AssFile *subs, wxWindow *export_dialog) { - // TODO + Auto3ProgressSink *sink = new Auto3ProgressSink(L, export_dialog); + sink->SetTitle(GetName()); + Auto3ThreadedProcessor thread(L, subs, config, sink); + + sink->ShowModal(); + thread.Wait(); + + delete sink; } - // Auto3ThreadedCall + // Auto3ThreadedProcessor - Auto3ThreadedCall::Auto3ThreadedCall(lua_State *_L, int _nargs, int _nresults) + Auto3ThreadedProcessor::Auto3ThreadedProcessor(lua_State *_L, AssFile *_file, Auto3ConfigDialog *_config, Auto3ProgressSink *_sink) + : wxThread(wxTHREAD_JOINABLE) + , L(_L) + , file(_file) + , config(_config) + , sink(_sink) { - // TODO + // Pure copypasta + int prio = Options.AsInt(_T("Automation Thread Priority")); + if (prio == 0) prio = 50; // normal + else if (prio == 1) prio = 30; // below normal + else if (prio == 2) prio = 10; // lowest + else prio = 50; // fallback normal + Create(); + SetPriority(prio); + Run(); } - wxThread::ExitCode Auto3ThreadedCall::Entry() + wxThread::ExitCode Auto3ThreadedProcessor::Entry() { - // TODO - return (wxThread::ExitCode)0; + bool failed = false; + + try { + sink->SetTask(_T("Preparing subtitle data")); + sink->SetProgress(0); + + // first put the function itself on the stack + lua_pushstring(L, "process_lines"); + lua_gettable(L, LUA_GLOBALSINDEX); + + // now put the three arguments on the stack + + // first argument: the metadata table + lua_newtable(L); + L_settable(L, -1, "res_x", file->GetScriptInfoAsInt(_T("PlayResX"))); + L_settable(L, -1, "res_y", file->GetScriptInfoAsInt(_T("PlayResY"))); + + // second and third arguments: styles and events tables + lua_newtable(L); + int styletab = lua_gettop(L); + lua_newtable(L); + int eventtab = lua_gettop(L); + + int numstyles = 0, numevents = 0; + + // fill the styles and events tables + int processed_lines = 1; + for (std::list::iterator i = file->Line.begin(); i != file->Line.end(); i++, processed_lines++) { + + AssEntry *e = *i; + + if (!e->Valid) continue; + + if (e->GetType() == ENTRY_STYLE) { + + AssStyle *style = e->GetAsStyle(e); + + // gonna need a table to put the style data into + lua_newtable(L); + // put the table into index N in the style table + lua_pushvalue(L, -1); + lua_rawseti(L, styletab, numstyles); + // and put it into its named index + lua_pushstring(L, style->name.mb_str(wxConvUTF8)); + lua_pushvalue(L, -2); + lua_settable(L, styletab); + + // so now the table is regged and stuff, put some data into it + L_settable (L, -1, "name", style->name); + L_settable (L, -1, "fontname", style->font); + L_settable (L, -1, "fontsize", style->fontsize); + L_settable (L, -1, "color1", style->primary.GetASSFormatted(true, true)); + L_settable (L, -1, "color2", style->secondary.GetASSFormatted(true, true)); + L_settable (L, -1, "color3", style->outline.GetASSFormatted(true, true)); + L_settable (L, -1, "color4", style->shadow.GetASSFormatted(true, true)); + L_settable_bool(L, -1, "bold", style->bold); + L_settable_bool(L, -1, "italic", style->italic); + L_settable_bool(L, -1, "underline", style->underline); + L_settable_bool(L, -1, "strikeout", style->strikeout); + L_settable (L, -1, "scale_x", style->scalex); + L_settable (L, -1, "scale_y", style->scaley); + L_settable (L, -1, "spacing", style->spacing); + L_settable (L, -1, "angle", style->angle); + L_settable (L, -1, "borderstyle", style->borderstyle); + L_settable (L, -1, "outline", style->outline_w); + L_settable (L, -1, "shadow", style->shadow_w); + L_settable (L, -1, "align", style->alignment); + L_settable (L, -1, "margin_l", style->Margin[0]); + L_settable (L, -1, "margin_r", style->Margin[1]); + L_settable (L, -1, "margin_v", style->Margin[2]); + L_settable (L, -1, "encoding", style->encoding); + + // and get that table off the stack again + lua_settop(L, -2); + + numstyles++; + + } else if (e->group == _T("[Events]")) { + + if (e->GetType() != ENTRY_DIALOGUE) { + + // not a dialogue/comment event + + // start checking for a blank line + wxString entryData = e->GetEntryData(); + if (entryData.IsEmpty()) { + lua_newtable(L); + L_settable(L, -1, "kind", wxString(_T("blank"))); + } else if (entryData[0] == _T(';')) { + // semicolon comment + lua_newtable(L); + L_settable(L, -1, "kind", wxString(_T("scomment"))); + L_settable(L, -1, "text", entryData.Mid(1)); + } else { + // not a blank line and not a semicolon comment + // just skip... + continue; + } + + } else { + + // ok, so it is a dialogue/comment event + // massive handling :( + + lua_newtable(L); + + assert(e->GetType() == ENTRY_DIALOGUE); + + AssDialogue *dia = e->GetAsDialogue(e); + + // kind of line + if (dia->Comment) { + L_settable(L, -1, "kind", wxString(_T("comment"))); + } else { + L_settable(L, -1, "kind", wxString(_T("dialogue"))); + } + + L_settable(L, -1, "layer", dia->Layer); + L_settable(L, -1, "start_time", dia->Start.GetMS()/10); + L_settable(L, -1, "end_time", dia->End.GetMS()/10); + L_settable(L, -1, "style", dia->Style); + L_settable(L, -1, "name", dia->Actor); + L_settable(L, -1, "margin_l", dia->Margin[0]); + L_settable(L, -1, "margin_r", dia->Margin[1]); + L_settable(L, -1, "margin_v", dia->Margin[2]); + L_settable(L, -1, "effect", dia->Effect); + L_settable(L, -1, "text", dia->Text); + + // so that's the easy part + // now for the stripped text and *ugh* the karaoke! + + // prepare for stripped text + wxString text_stripped = _T(""); + L_settable(L, -1, "text_stripped", 0); // dummy item + // prepare karaoke table + lua_newtable(L); + lua_pushstring(L, "karaoke"); + lua_pushvalue(L, -2); + lua_settable(L, -4); + // now the top of the stack is the karaoke table, and it's present in the dialogue table + + int kcount = 0; + int kdur = 0; + wxString kkind = _T(""); + wxString ktext = _T(""); + wxString ktext_stripped = _T(""); + + dia->ParseASSTags(); + for (std::vector::iterator block = dia->Blocks.begin(); block != dia->Blocks.end(); block++) { + + switch ((*block)->type) { + + case BLOCK_BASE: + lua_pushliteral(L, "BLOCK_BASE found processing dialogue blocks. This should never happen."); + lua_error(L); + break; + + case BLOCK_PLAIN: + ktext += (*block)->text; + ktext_stripped += (*block)->text; + text_stripped += (*block)->text; + break; + + case BLOCK_DRAWING: + ktext += (*block)->text; + break; + + case BLOCK_OVERRIDE: { + bool brackets_open = false; + std::vector &tags = (*block)->GetAsOverride(*block)->Tags; + + for (std::vector::iterator tag = tags.begin(); tag != tags.end(); tag++) { + + if (!(*tag)->Name.Mid(0,2).CmpNoCase(_T("\\k")) && (*tag)->IsValid()) { + + // it's a karaoke tag + if (brackets_open) { + ktext += _T("}"); + brackets_open = false; + } + L_settable_kara(L, -1, kcount, kdur, kkind, ktext, ktext_stripped); + kcount++; + kdur = (*tag)->Params[0]->AsInt(); // no error checking; this should always be int + kkind = (*tag)->Name.Mid(1); + ktext = _T(""); + ktext_stripped = _T(""); + + } else { + + // it's something else + // don't care if it's a valid tag or not + if (!brackets_open) { + ktext += _T("{"); + brackets_open = true; + } + ktext += (*tag)->ToString(); + + } + + } + + if (brackets_open) { + ktext += _T("}"); + } + + break;} + + } + + } + dia->ClearBlocks(); + + // add the final karaoke block to the table + // (even if there's no karaoke in the line, there's always at least one karaoke block) + // even if the line ends in {\k10} with no text after, an empty block should still be inserted + // (otherwise data are lost) + L_settable_kara(L, -1, kcount, kdur, kkind, ktext, ktext_stripped); + kcount++; + L_settable(L, -1, "n", kcount); // number of syllables in the karaoke + lua_settop(L, -2); // remove karaoke table from the stack again + L_settable(L, -1, "text_stripped", text_stripped); // store the real stripped text + + } + + // now the entry table has been created and placed on top of the stack + // now all that's missing it to insert it into the event table + lua_rawseti(L, eventtab, numevents); + numevents++; + + } else { + // not really a line type automation needs to take care of... ignore it + } + + sink->SetProgress(100.0f * processed_lines / file->Line.size() / 3); + + } + + // finally add the counter elements to the styles and events tables + lua_pushnumber(L, numstyles); + lua_rawseti(L, styletab, -1); + L_settable(L, eventtab, "n", numevents); + // and let the config object create a table for the @config argument + config->LuaReadBack(L); + + sink->SetTask(_T("Running script for processing")); + sink->SetProgress(100.0f/3); + lua_call(L, 4, 1); + sink->SetProgress(200.0f/3); + sink->SetTask(_T("Reading back data from script")); + + // phew, survived the call =) + // time to read back the results + + if (!lua_istable(L, -1)) { + throw _T("The script function did not return a table as expected. Unable to process results. (Nothing was changed.)"); + } + + // but start by removing all events + { + std::list::iterator cur, next; + next = file->Line.begin(); + while (next != file->Line.end()) { + cur = next++; + if ((*cur)->group == _T("[Events]")) { + wxString temp = (*cur)->GetEntryData(); + if (temp == _T("[Events]")) { + // skip the section header + continue; + } + if ((*cur)->GetType() != ENTRY_DIALOGUE && temp.Mid(0,1) != _T(";") && temp.Trim() != _T("")) { + // skip non-dialogue non-semicolon comment lines (such as Format) + continue; + } + delete (*cur); + file->Line.erase(cur); + } + } + } + + // so anyway, there is a single table on the stack now + // that table contains a lot of events... + // and it ought to contain an "n" key as well, telling how many events + // but be lenient, and don't expect one to be there, but rather count from zero and let it be nil-terminated + // if the "n" key is there, use it as a progress indicator hint, though + int output_line_count; + lua_pushstring(L, "n"); + lua_gettable(L, -2); + if (lua_isnumber(L, -1)) { + output_line_count = (int) lua_tonumber(L, -1); + } else { + // assume number of output lines == number of input lines + output_line_count = processed_lines; + } + lua_settop(L, -2); + + int outline = 0; + int faketime = file->Line.back()->StartMS; + + // If there's nothing at index 0, start at index 1 instead, to support both zero and one based indexing + lua_pushnumber(L, outline); + lua_gettable(L, -2); + if (!lua_istable(L, -1)) { + outline++; + output_line_count++; + } + lua_pop(L, 1); + + while (lua_pushnumber(L, outline), lua_gettable(L, -2), lua_istable(L, -1)) { + // top of the stack is a table, hopefully with an AssEntry in it + + // start by getting the kind + lua_pushstring(L, "kind"); + lua_gettable(L, -2); + if (!lua_isstring(L, -1)) { + sink->AddDebugOutput(wxString::Format(_T("The output data at index %d is mising a valid 'kind' field, and has been skipped\n"), outline)); + lua_settop(L, -2); + } else { + + wxString kind = wxString(lua_tostring(L, -1), wxConvUTF8).Lower(); + // remove "kind" from stack again + lua_settop(L, -2); + + if (kind == _T("dialogue") || kind == _T("comment")) { + + lua_pushstring(L, "layer"); + lua_gettable(L, -2); + lua_pushstring(L, "start_time"); + lua_gettable(L, -3); + lua_pushstring(L, "end_time"); + lua_gettable(L, -4); + lua_pushstring(L, "style"); + lua_gettable(L, -5); + lua_pushstring(L, "name"); + lua_gettable(L, -6); + lua_pushstring(L, "margin_l"); + lua_gettable(L, -7); + lua_pushstring(L, "margin_r"); + lua_gettable(L, -8); + lua_pushstring(L, "margin_v"); + lua_gettable(L, -9); + lua_pushstring(L, "effect"); + lua_gettable(L, -10); + lua_pushstring(L, "text"); + lua_gettable(L, -11); + + if (lua_isnumber(L, -10) && lua_isnumber(L, -9) && lua_isnumber(L, -8) && + lua_isstring(L, -7) && lua_isstring(L, -6) && lua_isnumber(L, -5) && + lua_isnumber(L, -4) && lua_isnumber(L, -3) && lua_isstring(L, -2) && + lua_isstring(L, -1)) + { + AssDialogue *e = new AssDialogue(); + e->Layer = (int)lua_tonumber(L, -10); + e->Start.SetMS(10*(int)lua_tonumber(L, -9)); + e->End.SetMS(10*(int)lua_tonumber(L, -8)); + e->Style = wxString(lua_tostring(L, -7), wxConvUTF8); + e->Actor = wxString(lua_tostring(L, -6), wxConvUTF8); + e->Margin[0] = (int)lua_tonumber(L, -5); + e->Margin[1] = (int)lua_tonumber(L, -4); + e->Margin[2] = e->Margin[3] = (int)lua_tonumber(L, -3); + e->Effect = wxString(lua_tostring(L, -2), wxConvUTF8); + e->Text = wxString(lua_tostring(L, -1), wxConvUTF8); + e->Comment = kind == _T("comment"); + lua_settop(L, -11); + e->StartMS = e->Start.GetMS(); + //e->ParseASSTags(); + e->UpdateData(); + file->Line.push_back(e); + } else { + sink->AddDebugOutput(wxString::Format(_T("The output data at index %d (kind '%s') has one or more missing/invalid fields, and has been skipped\n"), outline, kind.c_str())); + } + + } else if (kind == _T("scomment")) { + + lua_pushstring(L, "text"); + lua_gettable(L, -2); + if (lua_isstring(L, -1)) { + wxString text(lua_tostring(L, -1), wxConvUTF8); + lua_settop(L, -2); + AssEntry *e = new AssEntry(wxString(_T(";")) + text); + e->StartMS = faketime; + file->Line.push_back(e); + } else { + sink->AddDebugOutput(wxString::Format(_T("The output data at index %d (kind 'scomment') is missing a valid 'text' field, and has been skipped\n"), outline)); + } + + } else if (kind == _T("blank")) { + + AssEntry *e = new AssEntry(_T("")); + e->StartMS = faketime; + file->Line.push_back(e); + + } else { + sink->AddDebugOutput(wxString::Format(_T("The output data at index %d has an invalid value in the 'kind' field, and has been skipped\n"), outline)); + } + + } + + // remove table again + lua_settop(L, -2); + // progress report + if (outline >= output_line_count) { + sink->SetProgress(99.9f); + } else { + sink->SetProgress((200.0f + 100.0f*outline/output_line_count) / 3); + } + + outline++; + } + + sink->SetTask(_T("Completed")); + } + catch (const wchar_t *e) { + failed = true; + sink->AddDebugOutput(e); + } + catch (const char *e) { + failed = true; + wxString s(e, wxConvUTF8); + sink->AddDebugOutput(s); + } + catch (...) { + failed = true; + if (lua_isstring(L, -1)) { + wxString s(lua_tostring(L, -1), wxConvUTF8); + sink->AddDebugOutput(s); + } else { + sink->AddDebugOutput(_T("Unknown error")); + } + } + + if (failed) { + sink->SetTask(_T("Failed")); + } else { + sink->SetProgress(100); + } + sink->script_finished = true; + wxWakeUpIdle(); + if (failed) { + return (wxThread::ExitCode)1; + } else { + return (wxThread::ExitCode)0; + } } @@ -193,6 +1162,7 @@ namespace Automation4 { Auto3Script::Auto3Script(const wxString &filename) : Script(filename) , L(0) + , filter(0) { try { Create(); @@ -208,19 +1178,251 @@ namespace Automation4 { if (L) Destroy(); } + Auto3Script* Auto3Script::GetScriptObject(lua_State *L) + { + lua_getfield(L, LUA_REGISTRYINDEX, "aegisub"); + void *ptr = lua_touserdata(L, -1); + lua_pop(L, 1); + return (Auto3Script*)ptr; + } + + int Auto3Script::LuaTextExtents(lua_State *L) + { + double resx, resy, resd, resl; + + wxString intext(lua_tostring(L, -1), wxConvUTF8); + + AssStyle st; + + st.font = L_gettableS(L, "fontname"); + st.fontsize = L_gettableN(L, "fontsize"); + st.bold = L_gettableB(L, "bold"); + st.italic = L_gettableB(L, "italic"); + st.underline = L_gettableB(L, "underline"); + st.strikeout = L_gettableB(L, "strikeout"); + st.scalex = L_gettableN(L, "scale_x"); + st.scaley = L_gettableN(L, "scale_y"); + st.spacing = (int)L_gettableN(L, "spacing"); + st.encoding = (int)L_gettableN(L, "encoding"); + + if (!CalculateTextExtents(&st, intext, resx, resy, resd, resl)) { + lua_pushstring(L, "Some internal error occurred calculating text_extents"); + lua_error(L); + } + + lua_pushnumber(L, resx); + lua_pushnumber(L, resy); + lua_pushnumber(L, resd); + lua_pushnumber(L, resl); + return 4; + } + + int Auto3Script::LuaInclude(lua_State *L) + { + Auto3Script *s = GetScriptObject(L); + + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "Argument to include must be a string"); + lua_error(L); + return 0; + } + wxString fnames(lua_tostring(L, 1), wxConvUTF8); + + wxFileName fname(fnames); + if (fname.GetDirCount() == 0) { + // filename only + fname = s->include_path.FindAbsoluteValidPath(fnames); + } else if (fname.IsRelative()) { + // relative path + wxFileName sfname(s->GetFilename()); + fname.MakeAbsolute(sfname.GetPath(true)); + } else { + // absolute path, do nothing + } + if (!fname.IsOk() || !fname.FileExists()) { + lua_pushfstring(L, "Could not find Automation 3 script for inclusion: %s", fnames.mb_str(wxConvUTF8).data()); + lua_error(L); + } + + LuaScriptReader script_reader(fname.GetFullPath()); + if (lua_load(L, script_reader.reader_func, &script_reader, s->GetFilename().mb_str(wxConvUTF8))) { + lua_pushfstring(L, "An error occurred loading the Automation 3 script file \"%s\":\n\n%s", fname.GetFullPath().mb_str(wxConvUTF8).data(), lua_tostring(L, -1)); + lua_error(L); + return 0; + } + int pretop = lua_gettop(L) - 1; // don't count the function value itself + lua_call(L, 0, LUA_MULTRET); + return lua_gettop(L) - pretop; + } + + int Auto3Script::LuaColorstringToRGB(lua_State *L) + { + if (lua_gettop(L) < 1) { + lua_pushstring(L, "colorstring_to_rgb called without arguments"); + lua_error(L); + } + if (!lua_isstring(L, 1)) { + lua_pushstring(L, "colorstring_to_rgb requires a string type argument"); + lua_error(L); + } + + wxString colorstring(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + AssColor rgb; + rgb.Parse(colorstring); + lua_pushnumber(L, rgb.r); + lua_pushnumber(L, rgb.g); + lua_pushnumber(L, rgb.b); + lua_pushnumber(L, rgb.a); + return 4; + } + + int Auto3Script::LuaFrameFromMs(lua_State *L) + { + int ms = (int)lua_tonumber(L, -1); + lua_pop(L, 1); + if (VFR_Output.IsLoaded()) { + lua_pushnumber(L, VFR_Output.GetFrameAtTime(ms, true)); + return 1; + } else { + lua_pushnil(L); + return 1; + } + } + + int Auto3Script::LuaMsFromFrame(lua_State *L) + { + int frame = (int)lua_tonumber(L, -1); + lua_pop(L, 1); + if (VFR_Output.IsLoaded()) { + lua_pushnumber(L, VFR_Output.GetTimeAtFrame(frame, true)); + return 1; + } else { + lua_pushnil(L); + return 1; + } + } + void Auto3Script::Create() { - // TODO + Destroy(); + + loaded = true; + + try { + L = lua_open(); + + // register standard libs + lua_pushcfunction(L, luaopen_base); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_package); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_string); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_table); lua_call(L, 0, 0); + lua_pushcfunction(L, luaopen_math); lua_call(L, 0, 0); + // dofile and loadfile are replaced with include + lua_pushnil(L); + lua_setglobal(L, "dofile"); + lua_pushnil(L); + lua_setglobal(L, "loadfile"); + lua_pushcfunction(L, LuaInclude); + lua_setglobal(L, "include"); + + // reference to the script object + lua_pushlightuserdata(L, this); + lua_setfield(L, LUA_REGISTRYINDEX, "aegisub"); + + // make "aegisub" table + lua_newtable(L); + // put helper functions in it + lua_pushcfunction(L, LuaColorstringToRGB); + lua_setfield(L, -2, "colorstring_to_rgb"); + lua_pushcfunction(L, LuaTextExtents); + lua_setfield(L, -2, "text_extents"); + lua_pushcfunction(L, LuaFrameFromMs); + lua_setfield(L, -2, "frame_from_ms"); + lua_pushcfunction(L, LuaMsFromFrame); + lua_setfield(L, -2, "ms_from_frame"); + lua_pushinteger(L, 3); + lua_setfield(L, -2, "lua_automation_version"); + // store table + lua_setfield(L, LUA_GLOBALSINDEX, "aegisub"); + + // load user script + LuaScriptReader script_reader(GetFilename()); + if (lua_load(L, script_reader.reader_func, &script_reader, GetFilename().mb_str(wxConvUTF8))) { + wxString *err = new wxString(lua_tostring(L, -1), wxConvUTF8); + err->Prepend(_T("An error occurred loading the Automation 3 script file \"") + GetFilename() + _T("\":\n\n")); + throw err->c_str(); + } + // and run it + { + int err = lua_pcall(L, 0, 0, 0); + if (err) { + // error occurred, assumed to be on top of Lua stack + wxString *errs = new wxString(lua_tostring(L, -1), wxConvUTF8); + errs->Prepend(_T("An error occurred initialising the Automation 3 script file \"") + GetFilename() + _T("\":\n\n")); + throw errs->c_str(); + } + } + + // so, the script should be loaded + // now try to get the script data! + // first the version + lua_getglobal(L, "version"); + if (!lua_isnumber(L, -1)) { + throw _T("Script error: 'version' value not found or not a number"); + } + double engineversion = lua_tonumber(L, -1); + if (engineversion < 3 || engineversion > 4) { + // invalid version + throw _T("Script error: 'version' must be 3 for Automation 3 scripts"); + } + version = _T(""); + // skip 'kind', it's useless + // name + lua_getglobal(L, "name"); + if (!lua_isstring(L, -1)) { + name = GetFilename(); + } else { + name = wxString(lua_tostring(L, -1), wxConvUTF8); + } + // description (optional) + lua_getglobal(L, "description"); + if (lua_isstring(L, -1)) { + description = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + description = _T(""); + } + lua_pop(L, 4); + + // create filter feature object, that will check for process_lines function and configuration + filter = new Auto3Filter(name, description, L); + } + catch (...) { + Destroy(); + loaded = false; + throw; + } } void Auto3Script::Destroy() { - // TODO + if (!L) return; + + if (filter) { + delete filter; + filter = 0; + } + + lua_close(L); + L = 0; + + loaded = false; } void Auto3Script::Reload() { - // TODO + Destroy(); + Create(); } diff --git a/aegisub/auto4_auto3.h b/aegisub/auto4_auto3.h index 1524f06a0..93aa4809f 100644 --- a/aegisub/auto4_auto3.h +++ b/aegisub/auto4_auto3.h @@ -45,6 +45,10 @@ #include #include "../lua51/src/lua.h" #include "../lua51/src/lauxlib.h" +#include "ass_file.h" +#include "ass_entry.h" +#include "ass_dialogue.h" +#include "ass_style.h" namespace Automation4 { @@ -64,40 +68,94 @@ namespace Automation4 { }; + enum Auto3ScriptConfigurationOptionKind { + COK_INVALID = 0, + COK_LABEL, + COK_TEXT, + COK_INT, + COK_FLOAT, + COK_BOOL, + COK_COLOUR, + COK_STYLE + }; + + struct Auto3ScriptConfigurationOption { + wxString name; + Auto3ScriptConfigurationOptionKind kind; + wxString label; + wxString hint; + union { + bool isset; + int intval; + double floatval; + } min, max; + struct { + wxString stringval; + int intval; + double floatval; + bool boolval; + AssColor colourval; + } default_val, value; + }; + class Auto3ConfigDialog : public ScriptConfigDialog { // copypasta + private: + bool present; // is there any configuration option set at all? + + std::vector options; + + struct Control { + wxStaticText *label; + wxControl *control; + Auto3ScriptConfigurationOption *option; + Control() : label(0), control(0), option(0) {} + }; + std::vector controls; + + wxString ident; + protected: wxWindow* CreateWindow(wxWindow *parent); public: - Auto3ConfigDialog(lua_State *_L, bool include_buttons); + Auto3ConfigDialog(lua_State *L, const wxString &_ident); virtual ~Auto3ConfigDialog(); int LuaReadBack(lua_State *L); // read back internal structure to lua structures void ReadBack(); // from auto4 base + + wxString serialize(); // make a string from the option name+value pairs + void unserialize(wxString &settings); // set the option values from a serialized string }; class Auto3Filter : public FeatureFilter { - protected: - Auto3Filter(const wxString &_name, const wxString &_description, lua_State *_L); + private: + Auto3ConfigDialog *config; + AssFile *_file; + lua_State *L; + protected: ScriptConfigDialog* GenerateConfigDialog(wxWindow *parent); void Init(); public: + Auto3Filter(const wxString &_name, const wxString &_description, lua_State *_L); + void ProcessSubs(AssFile *subs, wxWindow *export_dialog); }; - class Auto3ThreadedCall : public wxThread { - // This is pretty much copy-paste from the non-legacy version + class Auto3ThreadedProcessor : public wxThread { private: lua_State *L; - int nargs; - int nresults; + AssFile *file; + Auto3ConfigDialog *config; + Auto3ProgressSink *sink; + public: - Auto3ThreadedCall(lua_State *_L, int _nargs, int _nresults); + Auto3ThreadedProcessor(lua_State *_L, AssFile *_file, Auto3ConfigDialog *_config, Auto3ProgressSink *_sink); virtual ExitCode Entry(); }; @@ -107,9 +165,17 @@ namespace Automation4 { Auto3Filter *filter; lua_State *L; + static int LuaTextExtents(lua_State *L); + static int LuaInclude(lua_State *L); + static int LuaColorstringToRGB(lua_State *L); + static int LuaFrameFromMs(lua_State *L); + static int LuaMsFromFrame(lua_State *L); + void Create(); void Destroy(); + static Auto3Script* GetScriptObject(lua_State *L); + public: Auto3Script(const wxString &filename); virtual ~Auto3Script(); diff --git a/aegisub/auto4_lua.cpp b/aegisub/auto4_lua.cpp index 918afb9cb..af02c0edb 100644 --- a/aegisub/auto4_lua.cpp +++ b/aegisub/auto4_lua.cpp @@ -97,74 +97,68 @@ namespace Automation4 { // LuaScriptReader - struct LuaScriptReader { - FILE *f; - bool first; - char *databuf; - static const size_t bufsize = 512; - LuaScriptReader(const wxString &filename) - { + LuaScriptReader::LuaScriptReader(const wxString &filename) + { #ifdef WIN32 - f = _tfopen(filename.c_str(), _T("rb")); + f = _tfopen(filename.c_str(), _T("rb")); #else - f = fopen(filename.fn_str(), "rb"); + f = fopen(filename.fn_str(), "rb"); #endif - first = true; - databuf = new char[bufsize]; - } - ~LuaScriptReader() - { - if (databuf) - delete databuf; - fclose(f); + first = true; + databuf = new char[bufsize]; + } + LuaScriptReader::~LuaScriptReader() + { + if (databuf) + delete databuf; + fclose(f); + } + + const char* LuaScriptReader::reader_func(lua_State *L, void *data, size_t *size) + { + LuaScriptReader *self = (LuaScriptReader*)(data); + unsigned char *b = (unsigned char *)self->databuf; + FILE *f = self->f; + + if (feof(f)) { + *size = 0; + return 0; } - static const char* reader_func(lua_State *L, void *data, size_t *size) - { - LuaScriptReader *self = (LuaScriptReader*)(data); - unsigned char *b = (unsigned char *)self->databuf; - FILE *f = self->f; - - if (feof(f)) { - *size = 0; - return 0; - } - - if (self->first) { - // check if file is sensible and maybe skip bom - if ((*size = fread(b, 1, 4, f)) == 4) { - if (b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF) { - // got an utf8 file with bom - // nothing further to do, already skipped the bom - fseek(f, -1, SEEK_CUR); - } else { - // oops, not utf8 with bom - // check if there is some other BOM in place and complain if there is... - if ((b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00) || // utf32be - (b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF) || // utf32le - (b[0] == 0xFF && b[1] == 0xFE) || // utf16be - (b[0] == 0xFE && b[1] == 0xFF) || // utf16le - (b[0] == 0x2B && b[1] == 0x2F && b[2] == 0x76) || // utf7 - (b[0] == 0x00 && b[2] == 0x00) || // looks like utf16be - (b[1] == 0x00 && b[3] == 0x00)) { // looks like utf16le - throw _T("The script file uses an unsupported character set. Only UTF-8 is supported."); - } - // assume utf8 without bom, and rewind file - fseek(f, 0, SEEK_SET); - } + if (self->first) { + // check if file is sensible and maybe skip bom + if ((*size = fread(b, 1, 4, f)) == 4) { + if (b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF) { + // got an utf8 file with bom + // nothing further to do, already skipped the bom + fseek(f, -1, SEEK_CUR); } else { - // hmm, rather short file this... - // doesn't have a bom, assume it's just ascii/utf8 without bom - return self->databuf; // *size is already set + // oops, not utf8 with bom + // check if there is some other BOM in place and complain if there is... + if ((b[0] == 0xFF && b[1] == 0xFE && b[2] == 0x00 && b[3] == 0x00) || // utf32be + (b[0] == 0x00 && b[1] == 0x00 && b[2] == 0xFE && b[3] == 0xFF) || // utf32le + (b[0] == 0xFF && b[1] == 0xFE) || // utf16be + (b[0] == 0xFE && b[1] == 0xFF) || // utf16le + (b[0] == 0x2B && b[1] == 0x2F && b[2] == 0x76) || // utf7 + (b[0] == 0x00 && b[2] == 0x00) || // looks like utf16be + (b[1] == 0x00 && b[3] == 0x00)) { // looks like utf16le + throw _T("The script file uses an unsupported character set. Only UTF-8 is supported."); + } + // assume utf8 without bom, and rewind file + fseek(f, 0, SEEK_SET); } - self->first = false; + } else { + // hmm, rather short file this... + // doesn't have a bom, assume it's just ascii/utf8 without bom + return self->databuf; // *size is already set } - - *size = fread(b, 1, bufsize, f); - - return self->databuf; + self->first = false; } - }; + + *size = fread(b, 1, bufsize, f); + + return self->databuf; + } // LuaScript @@ -235,6 +229,9 @@ namespace Automation4 { // aegisub.text_extents lua_pushcfunction(L, LuaTextExtents); lua_setfield(L, -2, "text_extents"); + // aegisub.lua_automation_version + lua_pushinteger(L, 4); + lua_setfield(L, -2, "lua_automation_version"); // store aegisub table to globals lua_settable(L, LUA_GLOBALSINDEX); _stackcheck.check(0); @@ -242,7 +239,6 @@ namespace Automation4 { // load user script LuaScriptReader script_reader(GetFilename()); if (lua_load(L, script_reader.reader_func, &script_reader, GetFilename().mb_str(wxConvUTF8))) { - //if (luaL_loadfile(L, GetFilename().mb_str(wxConvUTF8))) { wxString *err = new wxString(lua_tostring(L, -1), wxConvUTF8); err->Prepend(_T("An error occurred loading the Lua script file \"") + GetFilename() + _T("\":\n\n")); throw err->c_str(); @@ -401,7 +397,6 @@ namespace Automation4 { LuaScriptReader script_reader(fname.GetFullPath()); if (lua_load(L, script_reader.reader_func, &script_reader, s->GetFilename().mb_str(wxConvUTF8))) { - //if (luaL_loadfile(L, fname.GetFullPath().mb_str(wxConvUTF8))) { lua_pushfstring(L, "An error occurred loading the Lua script file \"%s\":\n\n%s", fname.GetFullPath().mb_str(wxConvUTF8).data(), lua_tostring(L, -1)); lua_error(L); return 0; diff --git a/aegisub/auto4_lua.h b/aegisub/auto4_lua.h index 20a960930..5e876cafc 100644 --- a/aegisub/auto4_lua.h +++ b/aegisub/auto4_lua.h @@ -48,6 +48,17 @@ class wxWindow; namespace Automation4 { + // Manage reading in a Lua script file + struct LuaScriptReader { + FILE *f; + bool first; + char *databuf; + static const size_t bufsize = 512; + LuaScriptReader(const wxString &filename); + ~LuaScriptReader(); + static const char* reader_func(lua_State *L, void *data, size_t *size); + }; + // Provides access to an AssFile object (and all lines contained) for a Lua script class LuaAssFile { private: diff --git a/aegisub/dialog_automation.cpp b/aegisub/dialog_automation.cpp index 077a7616c..9c3e8cf28 100644 --- a/aegisub/dialog_automation.cpp +++ b/aegisub/dialog_automation.cpp @@ -95,13 +95,6 @@ void DialogAutomation::RebuildList() list->DeleteAllItems(); // fill the list view - const std::vector &global_scripts = global_manager->GetScripts(); - for (std::vector::const_iterator i = global_scripts.begin(); i != global_scripts.end(); ++i) { - ExtraScriptInfo ei; - ei.script = *i; - ei.is_global = true; - AddScript(ei); - } const std::vector &local_scripts = local_manager->GetScripts(); for (std::vector::const_iterator i = local_scripts.begin(); i != local_scripts.end(); ++i) { ExtraScriptInfo ei; @@ -109,6 +102,13 @@ void DialogAutomation::RebuildList() ei.is_global = false; AddScript(ei); } + const std::vector &global_scripts = global_manager->GetScripts(); + for (std::vector::const_iterator i = global_scripts.begin(); i != global_scripts.end(); ++i) { + ExtraScriptInfo ei; + ei.script = *i; + ei.is_global = true; + AddScript(ei); + } } @@ -168,19 +168,26 @@ END_EVENT_TABLE() void DialogAutomation::OnAdd(wxCommandEvent &evt) { // build filename filter list - wxString fnfilter; + wxString fnfilter, catchall; const std::vector &factories = Automation4::ScriptFactory::GetFactories(); for (int i = 0; i < (int)factories.size(); i++) { const Automation4::ScriptFactory *fact = factories[i]; if (fact->GetEngineName().IsEmpty() || fact->GetFilenamePattern().IsEmpty()) continue; - fnfilter = wxString::Format(_T("%s%s scripts|%s|"), fnfilter.c_str(), fact->GetEngineName().c_str(), fact->GetFilenamePattern().c_str()); + fnfilter = wxString::Format(_T("%s%s scripts (%s)|%s|"), fnfilter.c_str(), fact->GetEngineName().c_str(), fact->GetFilenamePattern().c_str(), fact->GetFilenamePattern().c_str()); + catchall << fact->GetFilenamePattern() << _T(";"); } #ifdef __WINDOWS__ fnfilter += _T("All files|*.*"); #else fnfilter += _T("All files|*"); #endif + if (!catchall.IsEmpty()) { + catchall.RemoveLast(); + } + if (factories.size() > 1) { + fnfilter = _T("All script formats|") + catchall + _T("|") + fnfilter; + } wxString fname = wxFileSelector(_("Add Automation script"), Options.AsText(_T("Last open automation path")), wxEmptyString, wxEmptyString, fnfilter, wxOPEN|wxFILE_MUST_EXIST, this); diff --git a/aegisub/subtitle_provider_dtextsub.cpp b/aegisub/subtitle_provider_dtextsub.cpp new file mode 100644 index 000000000..1d4fe33be --- /dev/null +++ b/aegisub/subtitle_provider_dtextsub.cpp @@ -0,0 +1,150 @@ +// Copyright (c) 2007, Niels Martin Hansen +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the Aegisub Group nor the names of its contributors +// may be used to endorse or promote products derived from this software +// without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. +// +// ----------------------------------------------------------------------------- +// +// AEGISUB +// +// Website: http://aegisub.cellosoft.com +// Contact: mailto:jiifurusu@gmail.com +// + +// Use Gabest's RenderedTextSubtitles directly + +#if USE_DTEXTSUB == 1 + +#include +#include +#include "ass_file.h" +#include "subtitle_provider.h" +#include "video_provider.h" +#include +#include "vsfilter_editor_plugin.h" + +#pragma comment(lib, "vsfilter.lib") + +class SubtitleProviderDTextSub : public SubtitleProvider, public SubtitleProvider::Overlay { +private: + // A little copy-paste from the libass code... + class MyClass : public Class { + public: + MyClass() : Class(L"DirectTextSub") + { + }; + + virtual SubtitleProvider *Get(AssFile *subs) + { + return new SubtitleProviderDTextSub(subs); + }; + }; + static MyClass me; + + EditorPluginRenderer *renderer; + RECT rect; + + AssFile *subs; + VideoProvider *vpro; + +public: + SubtitleProviderDTextSub(AssFile *_subs); + virtual ~SubtitleProviderDTextSub(); + virtual void Bind(VideoProvider *_vpro); + + virtual void SetParams(int width, int height); + virtual void Render(wxImage &frame, int ms); + virtual void Unbind(); +}; + +SubtitleProviderDTextSub::MyClass SubtitleProviderDTextSub::me; + + +SubtitleProviderDTextSub::SubtitleProviderDTextSub(AssFile *_subs) +: vpro(0) +{ + subs = _subs; + renderer = renderer_new(); + if (!renderer) + throw _T("Failed to create VSFilter Editor Interface renderer"); +} + + +SubtitleProviderDTextSub::~SubtitleProviderDTextSub() +{ + Unbind(); + renderer_free(renderer); +} + + +void SubtitleProviderDTextSub::Bind(VideoProvider *_vpro) +{ + vpro = _vpro; + vpro->AttachOverlay(this); +} + + +void SubtitleProviderDTextSub::SetParams(int width, int height) +{ + SIZE screen; + SIZE script; + screen.cx = width; + screen.cy = height; + if (subs) { + int w, h; + subs->GetResolution(w, h); + script.cx = w; + script.cy = h; + } else { + script.cx = width; + script.cy = height; + } + renderer_set_resolution(renderer, &script, &screen, 0); +} + + +void SubtitleProviderDTextSub::Render(wxImage &frame, int ms) +{ + // TODO: some way to discover whether it was just a seek and nothing needs to be reloaded + renderer_clear(renderer); + + for (entryIter l = subs->Line.begin(); l != subs->Line.end(); ++l) { + } + + BYTE *bits = frame.GetData(); + + renderer_render_overlay(renderer, ms, bits); +} + + +void SubtitleProviderDTextSub::Unbind() +{ + if (vpro) + vpro->AttachOverlay(NULL); + vpro = 0; +} + + +#endif diff --git a/aegisub/vsfilter_editor_plugin.h b/aegisub/vsfilter_editor_plugin.h new file mode 100644 index 000000000..3c6a98685 --- /dev/null +++ b/aegisub/vsfilter_editor_plugin.h @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2007 Niels Martin Hansen + * http://www.aegisub.net/ + * + * This Program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This Program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GNU Make; see the file COPYING. If not, write to + * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. + * http://www.gnu.org/copyleft/gpl.html + * + */ + + +// This file provides a renderer-interface suited for applications +// that need to interactively update the subtitles and re-render +// the frame often, with modified subs. + + +// Don't include windows.h if this is an MFC project (it won't compile) +#ifndef __AFX_H__ +#include +#endif + +#ifndef PLUGIN_INTERFACE +#define _i_defined_plugin_interface +#define PLUGIN_INTERFACE extern "C" __declspec(dllimport) +#endif + + +struct EditorPluginRenderer; + + +// Create a new renderer and return an opaque handle to it +// Returns NULL on fail +PLUGIN_INTERFACE EditorPluginRenderer *renderer_new(); +// Free a renderer object +// Does not fail +PLUGIN_INTERFACE void renderer_free(EditorPluginRenderer *renderer); +// Set renderer resolution and clear all styles+dialogue data +// renderer and script_res are mandatory +// If screen_res is NULL, it's assumed to be the same as script_res +// If video_rect is NULL, it's assumed to have origin in (0,0) and same size as screen_res +PLUGIN_INTERFACE void renderer_set_resolution(EditorPluginRenderer *renderer, const SIZE *script_res, const SIZE *screen_res, const RECT *video_rect); +// Clears script and reinstates script resolution +PLUGIN_INTERFACE void renderer_clear(EditorPluginRenderer *renderer); +// Set wrap style +// Both arguments mandatory +PLUGIN_INTERFACE void renderer_set_wrap_style(EditorPluginRenderer *renderer, int wrap_style); +// Add a style definition +// All arguments mandatory +PLUGIN_INTERFACE void renderer_add_style(EditorPluginRenderer *renderer, const wchar_t *name, const wchar_t *fontname, double fontsize, COLORREF colors[4], BYTE alpha[4], + int bold, int italic, int underline, int strikeout, double scalex, double scaley, double spacing, double angle, + int borderstyle, double outline, double shadow, int alignment, const RECT *margins, int encoding, int relativeto); +// Add a dialogue line +// All arguments mandatory +PLUGIN_INTERFACE void renderer_add_dialogue(EditorPluginRenderer *renderer, int layer, int start, int end, const wchar_t *style, const wchar_t *name, + const RECT *margins, const wchar_t *effect, const wchar_t *text); +// Render a frame of subtitles laid over existing video +// time is the timestamp in milliseconds +// frame is a pointer to the 24 bpp RGB data to render over, assumed to have the screen_res dimensions, stride equal to width and top to bottom scanline ordering +PLUGIN_INTERFACE void renderer_render_overlay(EditorPluginRenderer *renderer, unsigned int time, BYTE *frame); +// Render a frame to an RGBA buffer +// time as above +// frame is a pointer to a buffer to contain the 32 bpp RGBA bitmap rendered; same assumptions as above +PLUGIN_INTERFACE void renderer_render_alpha(EditorPluginRenderer *renderer, unsigned int time, BYTE *frame); + + +#ifdef _i_defined_plugin_interface +#undef PLUGIN_INTERFACE +#undef _i_defined_plugin_interface +#endif + diff --git a/automation/auto3/automation-lua.txt b/automation/automation3.txt similarity index 100% rename from automation/auto3/automation-lua.txt rename to automation/automation3.txt diff --git a/automation/auto3/demos/1-minimal.lua b/automation/demos/auto3/1-minimal.lua similarity index 100% rename from automation/auto3/demos/1-minimal.lua rename to automation/demos/auto3/1-minimal.lua diff --git a/automation/auto3/demos/10-furigana.ass b/automation/demos/auto3/10-furigana.ass similarity index 100% rename from automation/auto3/demos/10-furigana.ass rename to automation/demos/auto3/10-furigana.ass diff --git a/automation/auto3/demos/10-furigana.lua b/automation/demos/auto3/10-furigana.lua similarity index 100% rename from automation/auto3/demos/10-furigana.lua rename to automation/demos/auto3/10-furigana.lua diff --git a/automation/auto3/demos/2-dump.lua b/automation/demos/auto3/2-dump.lua similarity index 99% rename from automation/auto3/demos/2-dump.lua rename to automation/demos/auto3/2-dump.lua index 701867b12..5074c328b 100644 --- a/automation/auto3/demos/2-dump.lua +++ b/automation/demos/auto3/2-dump.lua @@ -52,6 +52,7 @@ function process_lines(meta, styles, lines, config) out(string.format("Line %d: %s", i, lines[i].kind)) end end + out("Finished dumping") -- In the end, no modifications were done, so just return the original subtitle data return lines diff --git a/automation/auto3/demos/3-include.lua b/automation/demos/auto3/3-include.lua similarity index 100% rename from automation/auto3/demos/3-include.lua rename to automation/demos/auto3/3-include.lua diff --git a/automation/auto3/demos/4-text_extents.lua b/automation/demos/auto3/4-text_extents.lua similarity index 100% rename from automation/auto3/demos/4-text_extents.lua rename to automation/demos/auto3/4-text_extents.lua diff --git a/automation/auto3/demos/5-configuration.lua b/automation/demos/auto3/5-configuration.lua similarity index 100% rename from automation/auto3/demos/5-configuration.lua rename to automation/demos/auto3/5-configuration.lua diff --git a/automation/auto3/demos/6-simple-effect.lua b/automation/demos/auto3/6-simple-effect.lua similarity index 100% rename from automation/auto3/demos/6-simple-effect.lua rename to automation/demos/auto3/6-simple-effect.lua diff --git a/automation/auto3/demos/7-advanced-effect.lua b/automation/demos/auto3/7-advanced-effect.lua similarity index 100% rename from automation/auto3/demos/7-advanced-effect.lua rename to automation/demos/auto3/7-advanced-effect.lua diff --git a/automation/auto3/demos/8-skeleton.lua b/automation/demos/auto3/8-skeleton.lua similarity index 100% rename from automation/auto3/demos/8-skeleton.lua rename to automation/demos/auto3/8-skeleton.lua diff --git a/automation/auto3/demos/9-advanced-skeleton.lua b/automation/demos/auto3/9-advanced-skeleton.lua similarity index 100% rename from automation/auto3/demos/9-advanced-skeleton.lua rename to automation/demos/auto3/9-advanced-skeleton.lua diff --git a/automation/auto3/demos/readme.txt b/automation/demos/auto3/readme.txt similarity index 100% rename from automation/auto3/demos/readme.txt rename to automation/demos/auto3/readme.txt diff --git a/automation/auto3/include/karaskel-adv.lua b/automation/include/karaskel-adv.auto3 similarity index 99% rename from automation/auto3/include/karaskel-adv.lua rename to automation/include/karaskel-adv.auto3 index 68fcf5e0e..b26cf2fa5 100644 --- a/automation/auto3/include/karaskel-adv.lua +++ b/automation/include/karaskel-adv.auto3 @@ -32,7 +32,7 @@ -- positioning of karaoke. -- It automatically includes and re-setups karaskel.lua, so you should not include that yourself! -include("karaskel-base.lua") +include("karaskel-base.auto3") karaskel.engage_positioning = true -- The interface here has been greatly simplified, there is only one function to override, do_syllable diff --git a/automation/include/karaskel-adv.lua b/automation/include/karaskel-adv.lua index f739081b8..28eb6e6ab 100644 --- a/automation/include/karaskel-adv.lua +++ b/automation/include/karaskel-adv.lua @@ -28,11 +28,12 @@ ]] -- Aegisub Automation 4 Lua --- Wrapper for "advanced" karaskel +-- Nothing different between "regular" and "advanced" karaskel now, but auto3 will expect it -if not karaskel then - karaskel = {} +-- Compatibility hatch +if aegisub.lua_automation_version < 4 then + include("karaskel-adv.auto3") + return end -karaskel.advanced = true include("karaskel.lua") diff --git a/automation/auto3/include/karaskel-base.lua b/automation/include/karaskel-base.auto3 similarity index 99% rename from automation/auto3/include/karaskel-base.lua rename to automation/include/karaskel-base.auto3 index c225c3897..03a76e758 100644 --- a/automation/auto3/include/karaskel-base.lua +++ b/automation/include/karaskel-base.auto3 @@ -31,7 +31,7 @@ -- This file is meant as a support file for the various karaoke skeleton -- scripts, to avoid including unneeded code -include("utils.lua") +include("utils.auto3") -- karaskel -- This is a gloabl table defining various parameters, controlling what the diff --git a/automation/include/auto3-karaskel.lua b/automation/include/karaskel-base.lua similarity index 87% rename from automation/include/auto3-karaskel.lua rename to automation/include/karaskel-base.lua index 48ca61c55..899aed5c9 100644 --- a/automation/include/auto3-karaskel.lua +++ b/automation/include/karaskel-base.lua @@ -27,6 +27,13 @@ POSSIBILITY OF SUCH DAMAGE. ]] --- Automation 4 Lua compatibility script for Auto3 karaskel based scripts +-- Aegisub Automation 4 Lua +-- No "base" karaskel any longer, but auto3 scripts might expect it --- TODO +-- Compatibility hatch +if aegisub.lua_automation_version < 4 then + include("karaskel-base.auto3") + return +end + +include("karaskel.lua") diff --git a/automation/auto3/include/karaskel.lua b/automation/include/karaskel.auto3 similarity index 99% rename from automation/auto3/include/karaskel.lua rename to automation/include/karaskel.auto3 index c282237a7..afadcaeed 100644 --- a/automation/auto3/include/karaskel.lua +++ b/automation/include/karaskel.auto3 @@ -65,7 +65,7 @@ -- start_time - Start time of the syllable, in miliseconds, relative to the start of the line -- end_time - End time of the syllable, similar to start_time -include("karaskel-base.lua") +include("karaskel-base.auto3") -- Return a replacement text for a syllable function karaskel.do_syllable(meta, styles, config, line, syl) diff --git a/automation/include/karaskel.lua b/automation/include/karaskel.lua index b043db760..e8ad21488 100644 --- a/automation/include/karaskel.lua +++ b/automation/include/karaskel.lua @@ -29,20 +29,10 @@ -- Aegisub Automation 4 Lua karaoke skeleton -if version == 3 then - -- Attempt to emulate - -- This will only work if karaskel is being included after version has been defined - include("auto3-karaskel.lua") - - version = nil - if name then - script_name = name - name = nil - end - if description then - script_description = description - description = nil - end +-- Compatibility hatch +if aegisub.lua_automation_version < 4 then + include("karaskel.auto3") + return end include("utils.lua") diff --git a/automation/auto3/include/readme.txt b/automation/include/readme.txt similarity index 100% rename from automation/auto3/include/readme.txt rename to automation/include/readme.txt diff --git a/automation/auto3/include/utils.lua b/automation/include/utils.auto3 similarity index 100% rename from automation/auto3/include/utils.lua rename to automation/include/utils.auto3 diff --git a/automation/include/utils.lua b/automation/include/utils.lua index 2fb3d91be..0656ee731 100644 --- a/automation/include/utils.lua +++ b/automation/include/utils.lua @@ -27,6 +27,12 @@ POSSIBILITY OF SUCH DAMAGE. ]] +-- Compatibility hatch +if aegisub.lua_automation_version < 4 then + include "utils.auto3" + return +end + -- Make a shallow copy of a table function table.copy(oldtab) local newtab = {}