Aegisub/aegisub/auto4_auto3.cpp
Niels Martin Hansen 6dcb5ff4a8 Runtime errors in auto3 scripts no longer kill the program but are reported properly instead.
Apparently Lua 5.1 has changed the semantics of table.insert so it's incompatible with the way used in all Auto3 demo scripts etc. Now it's overridden in utils.auto3 and all instances of it in sample scripts etc. replaced with code having the expected behaviour.

Originally committed to SVN as r926.
2007-02-06 12:30:17 +00:00

1464 lines
42 KiB
C++

// Copyright (c) 2005, 2006, 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
//
#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
int Auto3ProgressSink::LuaSetStatus(lua_State *L)
{
Auto3ProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1));
wxString task(lua_tostring(L, 1), wxConvUTF8);
ps->SetTask(task);
return 0;
}
int Auto3ProgressSink::LuaOutputDebug(lua_State *L)
{
Auto3ProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1));
wxString msg(lua_tostring(L, 1), wxConvUTF8);
ps->AddDebugOutput(msg);
ps->AddDebugOutput(_T("\n"));
return 0;
}
int Auto3ProgressSink::LuaReportProgress(lua_State *L)
{
Auto3ProgressSink *ps = GetObjPointer(L, lua_upvalueindex(1));
float progress = lua_tonumber(L, 1);
ps->SetProgress(progress);
return 0;
}
Auto3ProgressSink::Auto3ProgressSink(lua_State *_L, wxWindow *parent)
: ProgressSink(parent)
, L(_L)
{
Auto3ProgressSink **ud = (Auto3ProgressSink**)lua_newuserdata(L, sizeof(Auto3ProgressSink*));
*ud = this;
// register progress reporting stuff
lua_getglobal(L, "aegisub");
lua_pushvalue(L, -2);
lua_pushcclosure(L, LuaReportProgress, 1);
lua_setfield(L, -2, "report_progress");
lua_pushvalue(L, -2);
lua_pushcclosure(L, LuaOutputDebug, 1);
lua_setfield(L, -2, "output_debug");
lua_pushvalue(L, -2);
lua_pushcclosure(L, LuaSetStatus, 1);
lua_setfield(L, -2, "set_status");
// reference so other objects can also find the progress sink
lua_pushvalue(L, -2);
lua_setfield(L, LUA_REGISTRYINDEX, "progress_sink");
// Remove aegisub table and userdata object from stack
lua_pop(L, 2);
}
Auto3ProgressSink::~Auto3ProgressSink()
{
// remove progress reporting stuff
lua_getglobal(L, "aegisub");
lua_pushnil(L);
lua_setfield(L, -2, "report_progress");
lua_pushnil(L);
lua_setfield(L, -2, "output_debug");
lua_pushnil(L);
lua_setfield(L, -2, "set_status");
lua_pop(L, 1);
lua_pushnil(L);
lua_setfield(L, LUA_REGISTRYINDEX, "progress_sink");
}
Auto3ProgressSink* Auto3ProgressSink::GetObjPointer(lua_State *L, int idx)
{
assert(lua_type(L, idx) == LUA_TUSERDATA);
void *ud = lua_touserdata(L, idx);
return *((Auto3ProgressSink**)ud);
}
// Auto3ConfigDialog
wxWindow* Auto3ConfigDialog::CreateWindow(wxWindow *parent)
{
if (options.size() == 0)
return 0;
wxPanel *res = new wxPanel(parent, -1);
wxFlexGridSizer *sizer = new wxFlexGridSizer(2, 5, 5);
for (std::vector<Auto3ScriptConfigurationOption>::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, const wxString &_ident)
: ident(_ident)
{
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?
}
int Auto3ConfigDialog::LuaReadBack(lua_State *L)
{
lua_newtable(L);
for (std::vector<Auto3ScriptConfigurationOption>::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()
{
wxString opthname = wxString::Format(_T("Automation Settings %s"), ident.c_str());
for (std::vector<Control>::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<Auto3ScriptConfigurationOption>::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.IsEmpty() && 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<Auto3ScriptConfigurationOption>::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;
}
}
}
}
// Auto3Filter
Auto3Filter::Auto3Filter(const wxString &_name, const wxString &_description, lua_State *_L)
: Feature(SCRIPTFEATURE_FILTER, _name)
, FeatureFilter(_name, _description, 0)
, L(_L)
{
// 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");
}
lua_pop(L, 2);
}
ScriptConfigDialog* Auto3Filter::GenerateConfigDialog(wxWindow *parent)
{
// configuration (let the config object do all the loading)
lua_getglobal(L, "configuration");
config = new Auto3ConfigDialog(L, GetName());
wxString opthname = wxString::Format(_T("Automation Settings %s"), GetName().c_str());
wxString serialized = AssFile::top->GetScriptInfo(opthname);
config->unserialize(serialized);
return config;
}
void Auto3Filter::Init()
{
// Nothing to do here
}
void Auto3Filter::ProcessSubs(AssFile *subs, wxWindow *export_dialog)
{
Auto3ProgressSink *sink = new Auto3ProgressSink(L, export_dialog);
sink->SetTitle(GetName());
Auto3ThreadedProcessor thread(L, subs, config, sink);
sink->ShowModal();
thread.Wait();
delete sink;
}
// Auto3ThreadedProcessor
Auto3ThreadedProcessor::Auto3ThreadedProcessor(lua_State *_L, AssFile *_file, Auto3ConfigDialog *_config, Auto3ProgressSink *_sink)
: wxThread(wxTHREAD_JOINABLE)
, L(_L)
, file(_file)
, config(_config)
, sink(_sink)
{
// 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 Auto3ThreadedProcessor::Entry()
{
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<AssEntry*>::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<AssDialogueBlock*>::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<AssOverrideTag*> &tags = (*block)->GetAsOverride(*block)->Tags;
for (std::vector<AssOverrideTag*>::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);
int ret = lua_pcall(L, 4, 1, 0);
if (ret) {
wxString emsg(lua_tostring(L, -1), wxConvUTF8);
emsg.Prepend(_T("The Automation 3 script produced an error:\n"));
emsg.Append(_T("\nThe subtitles have not been altered."));
lua_pushstring(L, emsg.mb_str(wxConvUTF8));
throw ret;
}
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<AssEntry*>::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;
}
}
// Auto3Script
Auto3Script::Auto3Script(const wxString &filename)
: Script(filename)
, L(0)
, filter(0)
{
try {
Create();
}
catch (wxChar *e) {
description = e;
loaded = false;
}
}
Auto3Script::~Auto3Script()
{
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()
{
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()
{
if (!L) return;
if (filter) {
delete filter;
filter = 0;
}
lua_close(L);
L = 0;
loaded = false;
}
void Auto3Script::Reload()
{
Destroy();
Create();
}
// Auto3ScriptFactory
class Auto3ScriptFactory : public ScriptFactory {
public:
Auto3ScriptFactory()
{
engine_name = _T("Legacy Automation 3");
filename_pattern = _T("*.auto3");
Register(this);
}
~Auto3ScriptFactory() { }
virtual Script* Produce(const wxString &filename) const
{
if (filename.Right(4).Lower() == _T(".auto3")) {
return new Auto3Script(filename);
} else {
return 0;
}
}
};
Auto3ScriptFactory _auto3_script_factory;
};