// Copyright (c) 2005, 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:zeratul@cellosoft.com // #include #include #include #include #include #include "automation.h" #include "ass_file.h" #include "ass_entry.h" #include "ass_dialogue.h" #include "ass_style.h" #include "options.h" #include "string_codec.h" #include "vfr.h" #ifdef WIN32 #include #include #else #include #include FT_FREETYPE_H #endif extern "C" { #include #include } int L_callfunc(lua_State *L, int nargs, int nresults); void L_settable(lua_State *L, int table, wxString &key, lua_Number val); void L_settable(lua_State *L, int table, wxString &key, wxString val); void L_settable_bool(lua_State *L, int table, wxString &key, bool val); void L_settable(lua_State *L, int table, const char *key, lua_Number val); void L_settable(lua_State *L, int table, const char *key, wxString val); void L_settable_bool(lua_State *L, int table, const char *key, bool val); void L_settable_kara(lua_State *L, int table, int index, int duration, wxString &kind, wxString &text, wxString &text_stripped); // these two assume the table to get from is on the top of the stack lua_Number L_gettableN(lua_State *L, const char *key); wxString L_gettableS(lua_State *L, const char *key); bool L_gettableB(lua_State *L, const char *key); namespace AutomationHelper { // helper functions for the scripts // expect a pointer to the automation script object to be on the private stack /* Helper function helper... Get the AutomationScript object associated with a Lua state. */ AutomationScript *GetScriptObject(lua_State *L) { lua_pushstring(L, "aegisub"); lua_rawget(L, LUA_REGISTRYINDEX); AutomationScript *s = (AutomationScript*)(lua_touserdata(L, -1)); if (!s) { lua_pushstring(L, "Unable to retrieve AutomationScript object from the registry. This should never happen!"); lua_error(L); // never returns } lua_pop(L, 1); return s; } /* "Debug hook" function, used for checking if the interpreter has been asked to cancel. If it has, a Lua error is reported. */ void hookfunc(lua_State *L, lua_Debug *ar) { AutomationScript *script = GetScriptObject(L); if (script->force_cancel) { if (ar->currentline < 0) { lua_pushstring(L, "Script forcibly terminated at an unknown line"); } else { lua_pushstring(L, "Script forcibly terminated at line "); lua_pushnumber(L, ar->currentline); lua_concat(L, 2); } lua_error(L); } } /* function aegisub.output_debug(text) Output text to a debug console. @text String. The text to output. Returns: nothing. */ int output_debug(lua_State *L) { AutomationScript *script = GetScriptObject(L); // check we were passed a string if (lua_gettop(L) < 1) { // idiot user (nothing on the stack) lua_pushstring(L, "output_debug called with no arguments"); lua_error(L); // never returns } else if (!lua_isstring(L, -1)) { // idiot user (didn't pass a string) lua_pushstring(L, "output_debug called with non string-compatible argument"); lua_error(L); // never returns } script->OutputDebugString(wxString(lua_tostring(L, 1), wxConvUTF8), true); return 0; } /* function aegisub.set_status(text) Sets the current status-message. (Used for progress-reporting.) @text String. The status message. Returns: nothing. */ int set_status(lua_State *L) { AutomationScript *script = GetScriptObject(L); // check we were passed a string if (lua_gettop(L) < 1) { // idiot user (nothing on the stack) lua_pushstring(L, "output_debug called with no arguments"); lua_error(L); // never returns } else if (!lua_isstring(L, -1)) { // idiot user (didn't pass a string) lua_pushstring(L, "output_debug called with non string-compatible argument"); lua_error(L); // never returns } script->OutputDebugString(wxString(lua_tostring(L, 1), wxConvUTF8), false); return 0; } /* function aegisub.colorstring_to_rgb(colorstring) Convert an ASS color-string to a set of RGB values. @colorstring String. The color-string to convert. Returns: Four values, all numbers, being the color components in this order: Red, Green, Blue, Alpha-channel */ int colorstring_to_rgb(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.ParseASS(colorstring); lua_pushnumber(L, rgb.r); lua_pushnumber(L, rgb.g); lua_pushnumber(L, rgb.b); lua_pushnumber(L, rgb.a); return 4; } /* function aegisub.report_progress(percent) Report the progress of the processing. @percent Number. How much of the data have been processed so far. Returns: nothing. */ int report_progress(lua_State *L) { AutomationScript *script = GetScriptObject(L); // check we were passed a string if (lua_gettop(L) < 1) { // idiot user (nothing on the stack) lua_pushstring(L, "report_progress called with no arguments"); lua_error(L); // never returns } else if (!lua_isnumber(L, -1)) { // idiot user (didn't pass a string) lua_pushstring(L, "report_progress requires a numeric argument"); lua_error(L); // never returns } lua_Number p = lua_tonumber(L, -1); if (p < 0) p = 0; if (p > 100) p = 100; p = (p+100)/3; script->ReportProgress(p); return 0; } /* function aegisub.text_extents(style, text) Calculate the on-screen pixel size of the given text using the given style. @style Table. A single style definition like those passed to process_lines. @text String. The text to calculate the extents for. This should not contain formatting codes, as they will be treated as part of the text. Returns 4 values: 1: Number. Width of the text, in pixels. 2: Number. Height of the text, in pixels. 3: Number. Descent of the text, in pixels. 4: Number. External leading for the text, in pixels. */ int text_extents(lua_State *L) { // vars for the result int resx = 0, resy = 0, resd = 0, resl = 0; // get the input // no error checking for the moment wxString intext(lua_tostring(L, -1), wxConvUTF8); // leave only style table lua_settop(L, -2); // read out the relevant parts of style wxString fontname(L_gettableS(L, "fontname")); double fontsize = L_gettableN(L, "fontsize"); bool bold = L_gettableB(L, "bold"); bool italic = L_gettableB(L, "italic"); bool underline = L_gettableB(L, "underline"); bool strikeout = L_gettableB(L, "strikeout"); double scale_x = L_gettableN(L, "scale_x"); double scale_y = L_gettableN(L, "scale_y"); int spacing = (int)L_gettableN(L, "spacing"); int charset = (int)L_gettableN(L, "encoding"); wxLogDebug(_T("text_extents for: %s:%f:%d%d%d%d:%f:%f:%d:%d"), fontname, fontsize, bold, italic, underline, strikeout, scale_x, scale_y, spacing, charset); #ifdef WIN32 HDC thedc = CreateCompatibleDC(0); if (!thedc) return 0; SetMapMode(thedc, MM_TEXT); fontsize = -MulDiv((int)(fontsize+0.5), GetDeviceCaps(thedc, LOGPIXELSY), 72); LOGFONT lf; ZeroMemory(&lf, sizeof(lf)); lf.lfHeight = fontsize; lf.lfWeight = bold ? FW_BOLD : FW_NORMAL; lf.lfItalic = italic; lf.lfUnderline = underline; lf.lfStrikeOut = strikeout; lf.lfCharSet = charset; lf.lfOutPrecision = OUT_TT_PRECIS; lf.lfClipPrecision = CLIP_DEFAULT_PRECIS; lf.lfQuality = ANTIALIASED_QUALITY; lf.lfPitchAndFamily = DEFAULT_PITCH|FF_DONTCARE; wcsncpy(lf.lfFaceName, fontname.wc_str(), 32); HFONT thefont = CreateFontIndirect(&lf); if (!thefont) return 0; SelectObject(thedc, thefont); SIZE sz; if (spacing) { resx = 0; for (unsigned int i = 0; i < intext.length(); i++) { wchar_t c = intext[i]; GetTextExtentPoint32(thedc, &c, 1, &sz); resx += sz.cx + spacing; resy = sz.cy; } } else { GetTextExtentPoint32(thedc, intext.wc_str(), intext.Length(), &sz); resx = sz.cx; resy = sz.cy; } // HACKISH FIX! This seems to work, but why? It shouldn't be needed?!? fontsize = L_gettableN(L, "fontsize"); resx = (int)(resx * fontsize/resy + 0.5); resy = (int)(fontsize + 0.5); TEXTMETRIC tm; GetTextMetrics(thedc, &tm); resd = tm.tmDescent; resl = tm.tmExternalLeading; DeleteObject(thedc); DeleteObject(thefont); #else // not WIN32 wxMemoryDC thedc; // fix fontsize to be 72 DPI fontsize = -FT_MulDiv((int)(fontsize+0.5), 72, thedc.GetPPI().y); // now try to get a font! // use the font list to get some caching... (chance is the script will need the same font very often) // USING wxTheFontList SEEMS TO CAUSE BAD LEAKS! //wxFont *thefont = wxTheFontList->FindOrCreateFont( wxFont thefont( fontsize, wxFONTFAMILY_DEFAULT, italic ? wxFONTSTYLE_ITALIC : wxFONTSTYLE_NORMAL, bold ? wxFONTWEIGHT_BOLD : wxFONTWEIGHT_NORMAL, underline, fontname, wxFONTENCODING_SYSTEM); thedc.SetFont(thefont); if (spacing) { // If there's inter-character spacing, kerning info must not be used, so calculate width per character for (unsigned int i = 0; i < intext.length(); i++) { int a, b, c, d; thedc.GetTextExtent(intext[i], &a, &b, &c, &d); resx += a + spacing; resy = b > resy ? b : resy; resd = c > resd ? c : resd; resl = d > resl ? d : resl; } } else { // If the inter-character spacing should be zero, kerning info can (and must) be used, so calculate everything in one go thedc.GetTextExtent(intext, &resx, &resy, &resd, &resl); } #endif // Compensate for scaling resx = (int)(scale_x / 100 * resx + 0.5); resy = (int)(scale_y / 100 * resy + 0.5); resd = (int)(scale_y / 100 * resd + 0.5); resl = (int)(scale_y / 100 * resl + 0.5); lua_pushnumber(L, resx); lua_pushnumber(L, resy); lua_pushnumber(L, resd); lua_pushnumber(L, resl); return 4; } /* function aegisub.frame_from_ms(ms) Return the video frame-number for the given time. @ms Number. Time in miliseconds to get the frame number for. Returns: A number, the frame numer. If there is no framerate data, returns nil. */ int frame_from_ms(lua_State *L) { int ms = (int)lua_tonumber(L, -1); lua_pop(L, 1); if (VFR_Output.loaded) { lua_pushnumber(L, VFR_Output.CorrectFrameAtTime(ms, true)); return 1; } else { lua_pushnil(L); return 1; } } /* function aegisub.ms_from_frame(frame) Returns the start-time for the given video frame-number. @frame Number. Frame-number to get start-time from. Returns: A number, the start-time of the frame. If there is no framerate data, returns nil. */ int ms_from_frame(lua_State *L) { int frame = (int)lua_tonumber(L, -1); lua_pop(L, 1); if (VFR_Output.loaded) { lua_pushnumber(L, VFR_Output.CorrectTimeAtFrame(frame, true)); return 1; } else { lua_pushnil(L); return 1; } } /* function include(filename) @filename String. Name of the file to include. Returns: Depends on the script included. */ int include(lua_State *L) { AutomationScript *script = GetScriptObject(L); if (!lua_isstring(L, 1)) { lua_pushstring(L, "First argument to the include function must be a string."); lua_error(L); } wxString fnames(lua_tostring(L, 1), wxConvUTF8); wxFileName fname(fnames); if (fname.GetDirCount() == 0) { // filename only fname = script->include_path.FindAbsoluteValidPath(fnames); } else if (fname.IsRelative()) { // relative path wxFileName sfname(script->filename); fname.MakeAbsolute(sfname.GetPath(true)); } else { // absolute path, invalid lua_pushstring(L, "Filename passed to include seems to have an absolute path, which is not allowed."); lua_error(L); } if (!fname.IsOk() || !fname.FileExists()) { { // need to make a new scope here, so the char buffer can go out of scope before lua_error() makes a longjmp wxCharBuffer errmsg = wxString::Format(_T("The file could not be included, not found. \"%s\""), fnames.c_str()).mb_str(wxConvUTF8); lua_pushstring(L, errmsg.data()); } lua_error(L); } AutomationScriptFile *sfile; sfile = AutomationScriptFile::CreateFromFile(fname.GetFullPath()); wxCharBuffer fnamebuf = fname.GetFullName().mb_str(wxConvUTF8); switch (luaL_loadbuffer(L, sfile->scriptdata, sfile->scriptlen, fnamebuf.data())) { // FIXME: these should be made into lua_error() things instead... probably case 0: // success! break; case LUA_ERRSYNTAX: throw AutomationError(wxString::Format(_T("Lua syntax error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); break; case LUA_ERRMEM: throw AutomationError(wxString::Format(_T("Lua memory allocation error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); break; default: throw AutomationError(wxString::Format(_T("Lua unknown error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); break; } delete sfile; // top of stack before the call (correct for the function itself being on stack) int pretop = lua_gettop(L)-1; // call the loaded script lua_call(L, 0, LUA_MULTRET); // calculate the number of results the script produced return lua_gettop(L)-pretop; } } /* Call a Lua function without risking killing the entire program Just throw a C++ exception instead :) Really just a thin wrapper around lua_pcall */ inline int L_callfunc(lua_State *L, int nargs, int nresults) { int res = lua_pcall(L, nargs, nresults, 0); switch (res) { case LUA_ERRRUN: throw AutomationError(wxString::Format(_T("Lua runtime error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); case LUA_ERRMEM: throw AutomationError(wxString::Format(_T("Lua memory allocation error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); case LUA_ERRERR: // shouldn't happen as an error handling function isn't being used throw AutomationError(wxString::Format(_T("Lua error calling error handler: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); default: // success! return res; } } inline void L_settable(lua_State *L, int table, wxString &key, lua_Number val) { L_settable(L, table, key.mb_str(wxConvUTF8), val); } inline void L_settable(lua_State *L, int table, wxString &key, wxString val) { //wxLogMessage(_T("Wrapping adding of string at index '%s': %s"), key, val); L_settable(L, table, key.mb_str(wxConvUTF8), val); } inline void L_settable_bool(lua_State *L, int table, wxString &key, bool val) { L_settable_bool(L, table, key.mb_str(wxConvUTF8), val); } 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); } } 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); } } 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); } } 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); } } 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; } 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; } 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; } AutomationError::AutomationError(wxString msg) : message(msg) { // nothing to do here... } wxString AutomationScriptConfiguration::serialize() { 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 AutomationScriptConfiguration::unserialize(wxString &settings) { //wxLogMessage(_T("Unserializing config string: %s"), settings); wxStringTokenizer toker(settings, _T("|"), wxTOKEN_STRTOK); while (toker.HasMoreTokens()) { // get the parts of this setting wxString setting = toker.GetNextToken(); //wxLogMessage(_T("Got token: %s"), setting); wxString optname = setting.BeforeFirst(_T(':')); wxString optval = setting.AfterFirst(_T(':')); //wxLogMessage(_T("Split into: \"%s\" and \"%s\""), optname, optval); // 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()) { //wxLogMessage(_T("Found the option!")); // ok, found the option! switch (opt->kind) { case COK_TEXT: case COK_STYLE: opt->value.stringval = inline_string_decode(optval); //wxLogMessage(_T("Decoded string to: %s"), opt->value.stringval); 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.ParseASS(optval); break; } } } } void AutomationScriptConfiguration::load_from_lua(lua_State *L) { //wxLogMessage(_T("Loading configuration options from script")); present = false; if (!lua_istable(L, -1)) { return; } //wxLogMessage(_T("The script does have config options, good!")); int i = 1; while (true) { // get an element from the array //wxLogMessage(_T("Getting option %d (stacktop is %d)"), i, lua_gettop(L)); lua_pushnumber(L, i); lua_gettable(L, -2); // check if it was a table if (!lua_istable(L, -1)) { //wxLogMessage(_T("Damn! Not an option... breaking out (actual type was %d)"), lua_type(L, -1)); lua_pop(L, 1); break; } //wxLogMessage(_T("Yay! It was an option, adding another blank option thing to the list")); // add a new config option and fill it { AutomationScriptConfigurationOption opt; options.push_back(opt); } AutomationScriptConfigurationOption &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; } //wxLogMessage(_T("Got kind: %d"), opt.kind); // 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; } //wxLogMessage(_T("Got name: %s"), opt.name); // 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.ParseASS(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++; } } void AutomationScriptConfiguration::store_to_lua(lua_State *L) { // we'll always need a new table, no matter what lua_newtable(L); //wxLogMessage(_T("Created table for configuration data (top=%d)"), lua_gettop(L)); for (std::vector::iterator opt = options.begin(); opt != options.end(); opt++) { //wxLogMessage(_T("Storing option named '%s' (top=%d)"), opt->name, lua_gettop(L)); switch (opt->kind) { case COK_INVALID: case COK_LABEL: //wxLogMessage(_T("Nothing to store")); break; case COK_TEXT: case COK_STYLE: //wxLogMessage(_T("Storing string value")); L_settable(L, -1, opt->name, opt->value.stringval); break; case COK_INT: //wxLogMessage(_T("Storing int value")); L_settable(L, -1, opt->name, opt->value.intval); break; case COK_FLOAT: //wxLogMessage(_T("Storing float value")); L_settable(L, -1, opt->name, opt->value.floatval); break; case COK_BOOL: //wxLogMessage(_T("Storing bool value")); L_settable_bool(L, -1, opt->name, opt->value.boolval); break; case COK_COLOUR: //wxLogMessage(_T("Storing colourvalue")); L_settable(L, -1, opt->name, opt->value.colourval.GetASSFormatted(false, false)); break; default: //wxLogMessage(_T("Felt into default handler?!?")); break; } } //wxLogMessage(_T("Finished storing configuration data (top=%d)"), lua_gettop(L)); } AutomationScript::AutomationScript(AutomationScriptFile *script) { force_cancel = false; filename = script->filename; progress_reporter = 0; debug_reporter = 0; // get a lua object L = lua_open(); // put a pointer to this object in the registry index lua_pushstring(L, "aegisub"); lua_pushlightuserdata(L, this); lua_rawset(L, LUA_REGISTRYINDEX); // set up the cancelling hook, call every 100 instructions lua_sethook(L, AutomationHelper::hookfunc, LUA_MASKCOUNT, 100); // provide some standard libraries luaopen_base(L); luaopen_string(L); luaopen_table(L); luaopen_math(L); #ifdef _DEBUG luaopen_debug(L); #endif // but no I/O, OS or debug facilities, those aren't safe // disable the dofile() function lua_pushstring(L, "dofile"); lua_pushnil(L); lua_settable(L, LUA_GLOBALSINDEX); // create an include function better suited aegisub later // the path object is needed for this include_path.EnsureFileAccessible(script->filename); { wxStringTokenizer toker(Options.AsText(_T("Automation Include Path")), _T("|"), false); while (toker.HasMoreTokens()) { wxFileName path(toker.GetNextToken()); if (!path.IsOk()) continue; if (path.IsRelative()) continue; if (!path.DirExists()) continue; if (include_path.Member(path.GetLongPath())) continue; include_path.Add(path.GetLongPath()); } } // add the include function to the global environment lua_pushstring(L, "include"); lua_pushcfunction(L, AutomationHelper::include); lua_settable(L, LUA_GLOBALSINDEX); // create the "aegisub" table and fill it with some function pointers // create the table and add it to the global environment lua_pushstring(L, "aegisub"); lua_newtable(L); lua_settable(L, LUA_GLOBALSINDEX); // get it back onto the stack lua_pushstring(L, "aegisub"); lua_gettable(L, LUA_GLOBALSINDEX); // get the index of the table on the stack int tabid = lua_gettop(L); // now register some functions! lua_pushstring(L, "output_debug"); lua_pushcfunction(L, AutomationHelper::output_debug); lua_settable(L, tabid); lua_pushstring(L, "set_status"); lua_pushcfunction(L, AutomationHelper::set_status); lua_settable(L, tabid); lua_pushstring(L, "report_progress"); lua_pushcfunction(L, AutomationHelper::report_progress); lua_settable(L, tabid); lua_pushstring(L, "colorstring_to_rgb"); lua_pushcfunction(L, AutomationHelper::colorstring_to_rgb); lua_settable(L, tabid); lua_pushstring(L, "text_extents"); lua_pushcfunction(L, AutomationHelper::text_extents); lua_settable(L, tabid); lua_pushstring(L, "frame_from_ms"); lua_pushcfunction(L, AutomationHelper::frame_from_ms); lua_settable(L, tabid); lua_pushstring(L, "ms_from_frame"); lua_pushcfunction(L, AutomationHelper::ms_from_frame); lua_settable(L, tabid); // and no more need for that tabid lua_settop(L, tabid-1); // ok, finally the environment is set up! // now try to load the script { switch (luaL_loadbuffer(L, script->scriptdata, script->scriptlen, "user script")) { case 0: // success! break; case LUA_ERRSYNTAX: throw AutomationError(wxString::Format(_T("Lua syntax error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); break; case LUA_ERRMEM: throw AutomationError(wxString::Format(_T("Lua memory allocation error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); break; default: throw AutomationError(wxString::Format(_T("Lua unknown error: %s"), wxString(lua_tostring(L, -1), wxConvUTF8).c_str())); break; } } // it's loaded and is now a function at the top of the stack // it doesn't take any arguments and doesn't return anything // so let's try executing it by calling! L_callfunc(L, 0, 0); // so, the script should be loaded // now try to get the script data! // first the version lua_pushstring(L, "version"); lua_gettable(L, LUA_GLOBALSINDEX); if (!lua_isnumber(L, -1)) { throw AutomationError(wxString(_T("Script error: 'version' value not found or not a number"))); } version = lua_tonumber(L, -1); lua_settop(L, -2); if (version < 3 || version > 4) { // invalid version throw AutomationError(wxString(_T("Script error: 'version' value invalid for this version of Automation"))); } // kind lua_pushstring(L, "kind"); lua_gettable(L, LUA_GLOBALSINDEX); if (!lua_isstring(L, -1)) { throw AutomationError(wxString(_T("Script error: 'kind' value not found or not a string"))); } kind = wxString(lua_tostring(L, -1), wxConvUTF8); lua_settop(L, -2); // name lua_pushstring(L, "name"); lua_gettable(L, LUA_GLOBALSINDEX); if (!lua_isstring(L, -1)) { throw AutomationError(wxString(_T("Script error: 'name' value not found or not a string"))); } name = wxString(lua_tostring(L, -1), wxConvUTF8); lua_settop(L, -2); // description (optional) lua_pushstring(L, "description"); lua_gettable(L, LUA_GLOBALSINDEX); if (lua_isstring(L, -1)) { description = wxString(lua_tostring(L, -1), wxConvUTF8); lua_settop(L, -2); } else { description = _T(""); } // process_lines (just check if it's there, no need to save it anywhere) lua_pushstring(L, "process_lines"); lua_gettable(L, LUA_GLOBALSINDEX); if (!lua_isfunction(L, -1)) { throw AutomationError(wxString(_T("Script error: No 'process_lines' function provided"))); } lua_settop(L, -2); // configuration (let the config object do all the loading) lua_pushstring(L, "configuration"); lua_gettable(L, LUA_GLOBALSINDEX); //wxLogMessage(_T("Calling configuration.load_from_lua()")); configuration.load_from_lua(L); lua_settop(L, -2); // done! } AutomationScript::~AutomationScript() { lua_close(L); } void AutomationScript::OutputDebugString(wxString str, bool isdebug) { //wxLogMessage(_T("automation message: ") + str); if (debug_reporter) { debug_reporter(str, isdebug, this, debug_target); } return; } void AutomationScript::ReportProgress(float progress) { //wxLogMessage(wxString::Format(_T("automation progress: %.1f%%"), progress)); if (progress_reporter) { progress_reporter(progress, this, progress_target); } return; } int AutomationScript::L_panicfunc(lua_State *L) { wxLogError(_T("Lua produced an error. Attempting to recover.")); longjmp(AutomationHelper::GetScriptObject(L)->panicjmp, lua_gettop(L)); } void AutomationScript::process_lines(AssFile *input) { // prepare for panic... if (int ret = setjmp(panicjmp)) { wxLogError(wxString::Format(_T("Returned out of Lua environment. Size of stack before: %d"), ret)); #ifdef _DEBUG wxLogError( #else wxLogFatalError( #endif _T("Due to an internal error in the Lua engine, the internal state of Aegisub might have become inconsistent ") _T("and cannot continue. If you can reproduce this error, please report it to the developers.")); lua_close(L); return; } else { lua_atpanic(L, AutomationScript::L_panicfunc); } // start by generating lua representations of the data... // maybe it's safest to start by making plenty of space on the stack if (!lua_checkstack(L, 100)) { throw AutomationError(wxString(_T("Lua error: Unable to allocate stack space"))); } OutputDebugString(wxString(_T("Preparing subtitle data"))); // 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", input->GetScriptInfoAsInt(_T("PlayResX"))); L_settable(L, -1, "res_y", input->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 = input->Line.begin(); i != input->Line.end(); i++, processed_lines++) { AssEntry *e = *i; if (!e->Valid) continue; if (e->Type == 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->MarginL); L_settable (L, -1, "margin_r", style->MarginR); L_settable (L, -1, "margin_v", style->MarginV); 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->Type != ENTRY_DIALOGUE) { // not a dialogue/comment event // start checking for a blank line if (e->data.IsEmpty()) { lua_newtable(L); L_settable(L, -1, "kind", wxString(_T("blank"))); } else if (e->data[0] == _T(';')) { // semicolon comment lua_newtable(L); L_settable(L, -1, "kind", wxString(_T("scomment"))); L_settable(L, -1, "text", e->data.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->Type == 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->MarginL); L_settable(L, -1, "margin_r", dia->MarginR); L_settable(L, -1, "margin_v", dia->MarginV); 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(""); for (std::vector::iterator block = dia->Blocks.begin(); block != dia->Blocks.end(); block++) { switch ((*block)->type) { case BLOCK_BASE: throw _T("BLOCK_BASE found processing dialogue blocks. This should never happen."); 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;} } } // 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 } ReportProgress(100.0f * processed_lines / input->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 //wxLogMessage(_T("Calling configuration.store_to_lua()")); configuration.store_to_lua(L); // so now the metadata, styles and events tables are filled with data // ready to call the processing function! OutputDebugString(wxString(_T("Running script for processing"))); ReportProgress(100.0f/3); L_callfunc(L, 4, 1); ReportProgress(200.0f/3); OutputDebugString(wxString(_T("Reading back data from script"))); // phew, survived the call =) // time to read back the results wxLogDebug(_T("Returned from Lua script call")); if (!lua_istable(L, -1)) { throw AutomationError(wxString(_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 = input->Line.begin(); while (next != input->Line.end()) { cur = next++; if ((*cur)->group == _T("[Events]")) { if ((*cur)->data == _T("[Events]")) { // skip the section header continue; } if ((*cur)->Type != ENTRY_DIALOGUE && (*cur)->data.Mid(0,1) != _T(";") && (*cur)->data.Trim() != _T("")) { // skip non-dialogue non-semicolon comment lines (such as Format) continue; } delete (*cur); input->Line.erase(cur); } } } wxLogDebug(_T("Finished removing old events from subtitles")); // 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); wxLogDebug(_T("Retrieved number of lines in result: %d"), output_line_count); // loop through the stack and report back the type of each element wxLogDebug(_T("Size of Lua stack: %d"), lua_gettop(L)); for (int si = lua_gettop(L); si > 0; si--) { wxString type = wxString(lua_typename(L, lua_type(L, si)), wxConvUTF8); wxLogDebug(_T("Stack index %d, type %s"), si, type.c_str()); } wxLogDebug(_T("Stack dump finished")); int outline = 0; int faketime = input->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 wxLogDebug(_T("Processing output line %d"), outline); // start by getting the kind lua_pushstring(L, "kind"); lua_gettable(L, -2); if (!lua_isstring(L, -1)) { OutputDebugString(wxString::Format(_T("The output data at index %d is mising a valid 'kind' field, and has been skipped"), outline)); lua_settop(L, -2); } else { wxString kind = wxString(lua_tostring(L, -1), wxConvUTF8).Lower(); // remove "kind" from stack again lua_settop(L, -2); wxLogDebug(_T("Kind of line: %s"), kind.c_str()); 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); wxLogDebug(_T("Read out all fields for dialogue event")); 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->MarginL = (int)lua_tonumber(L, -5); e->MarginR = (int)lua_tonumber(L, -4); e->MarginV = (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(); input->Line.push_back(e); wxLogDebug(_T("Produced new dialogue event in output subs")); } else { OutputDebugString(wxString::Format(_T("The output data at index %d (kind '%s') has one or more missing/invalid fields, and has been skipped"), 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; input->Line.push_back(e); wxLogDebug(_T("Produced new semicolon comment in output subs")); } else { OutputDebugString(wxString::Format(_T("The output data at index %d (kind 'scomment') is missing a valid 'text' field, and has been skipped"), outline)); } } else if (kind == _T("blank")) { AssEntry *e = new AssEntry(_T("")); e->StartMS = faketime; input->Line.push_back(e); wxLogDebug(_T("Produced new blank line in output subs")); } else { OutputDebugString(wxString::Format(_T("The output data at index %d has an invalid value in the 'kind' field, and has been skipped"), outline)); } } // remove table again lua_settop(L, -2); // progress report if (outline >= output_line_count) { ReportProgress(99.9f); } else { ReportProgress((200.0f + 100.0f*outline/output_line_count) / 3); } outline++; wxLogDebug(_T("Size of Lua stack: %d"), lua_gettop(L)); } ReportProgress(100); OutputDebugString(wxString(_T("Script execution complete"))); return; } AutomationScriptFile::~AutomationScriptFile() { // if file had a bom if (utf8bom) scriptdata -= 3; delete scriptdata; } AutomationScriptFile *AutomationScriptFile::CreateFromString(wxString &script) { wxCharBuffer rawscript = script.mb_str(wxConvUTF8); AutomationScriptFile *res = new AutomationScriptFile(); res->scriptlen = strlen(rawscript); res->scriptdata = new char[res->scriptlen]; res->filename = _T(""); return res; } AutomationScriptFile *AutomationScriptFile::CreateFromFile(wxString filename) { wxFile file(filename); if (!file.IsOpened()) { return 0; } // prepare variables AutomationScriptFile *res = new AutomationScriptFile(); res->filename = filename; res->scriptlen = file.Length(); res->scriptdata = new char[res->scriptlen]; // read from file file.Read(res->scriptdata, res->scriptlen); // check for UTF-8 BOM res->utf8bom = ((res->scriptdata[0]&0xFF) == 0xEF) && ((res->scriptdata[1]&0xFF) == 0xBB) && ((res->scriptdata[2]&0xFF) == 0xBF); if (res->utf8bom) { // skip it if found res->scriptdata += 3; res->scriptlen -= 3; } return res; }