1 // Copyright (c) 2006, Niels Martin Hansen 2 // All rights reserved. 3 // 4 // Redistribution and use in source and binary forms, with or without 5 // modification, are permitted provided that the following conditions are met: 6 // 7 // * Redistributions of source code must retain the above copyright notice, 8 // this list of conditions and the following disclaimer. 9 // * Redistributions in binary form must reproduce the above copyright notice, 10 // this list of conditions and the following disclaimer in the documentation 11 // and/or other materials provided with the distribution. 12 // * Neither the name of the Aegisub Group nor the names of its contributors 13 // may be used to endorse or promote products derived from this software 14 // without specific prior written permission. 15 // 16 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 19 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 20 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 21 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 22 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 23 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 24 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 25 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26 // POSSIBILITY OF SUCH DAMAGE. 27 // 28 // Aegisub Project http://www.aegisub.org/ 29 30 #include "auto4_base.h" 31 32 #include "ass_file.h" 33 #include "ass_style.h" 34 #include "compat.h" 35 #include "dialog_progress.h" 36 #include "include/aegisub/context.h" 37 #include "options.h" 38 #include "string_codec.h" 39 #include "subs_controller.h" 40 41 #include <libaegisub/dispatch.h> 42 #include <libaegisub/format.h> 43 #include <libaegisub/fs.h> 44 #include <libaegisub/path.h> 45 #include <libaegisub/make_unique.h> 46 47 #include <boost/algorithm/string/replace.hpp> 48 #include <boost/algorithm/string/trim.hpp> 49 #include <boost/tokenizer.hpp> 50 #include <future> 51 52 #include <wx/dcmemory.h> 53 #include <wx/log.h> 54 #include <wx/sizer.h> 55 56 #ifdef __WINDOWS__ 57 #define WIN32_LEAN_AND_MEAN 58 #include <windows.h> 59 60 #include <libaegisub/charset_conv_win.h> 61 #endif 62 63 namespace Automation4 { CalculateTextExtents(AssStyle * style,std::string const & text,double & width,double & height,double & descent,double & extlead)64 bool CalculateTextExtents(AssStyle *style, std::string const& text, double &width, double &height, double &descent, double &extlead) 65 { 66 width = height = descent = extlead = 0; 67 68 double fontsize = style->fontsize * 64; 69 double spacing = style->spacing * 64; 70 71 #ifdef WIN32 72 // This is almost copypasta from TextSub 73 auto dc = CreateCompatibleDC(nullptr); 74 if (!dc) return false; 75 76 SetMapMode(dc, MM_TEXT); 77 78 LOGFONTW lf = {0}; 79 lf.lfHeight = (LONG)fontsize; 80 lf.lfWeight = style->bold ? FW_BOLD : FW_NORMAL; 81 lf.lfItalic = style->italic; 82 lf.lfUnderline = style->underline; 83 lf.lfStrikeOut = style->strikeout; 84 lf.lfCharSet = style->encoding; 85 lf.lfOutPrecision = OUT_TT_PRECIS; 86 lf.lfClipPrecision = CLIP_DEFAULT_PRECIS; 87 lf.lfQuality = ANTIALIASED_QUALITY; 88 lf.lfPitchAndFamily = DEFAULT_PITCH|FF_DONTCARE; 89 wcsncpy(lf.lfFaceName, agi::charset::ConvertW(style->font).c_str(), 31); 90 91 auto font = CreateFontIndirect(&lf); 92 if (!font) return false; 93 94 auto old_font = SelectObject(dc, font); 95 96 std::wstring wtext(agi::charset::ConvertW(text)); 97 if (spacing != 0 ) { 98 width = 0; 99 for (auto c : wtext) { 100 SIZE sz; 101 GetTextExtentPoint32(dc, &c, 1, &sz); 102 width += sz.cx + spacing; 103 height = sz.cy; 104 } 105 } 106 else { 107 SIZE sz; 108 GetTextExtentPoint32(dc, &wtext[0], (int)wtext.size(), &sz); 109 width = sz.cx; 110 height = sz.cy; 111 } 112 113 TEXTMETRIC tm; 114 GetTextMetrics(dc, &tm); 115 descent = tm.tmDescent; 116 extlead = tm.tmExternalLeading; 117 118 SelectObject(dc, old_font); 119 DeleteObject(font); 120 DeleteObject(dc); 121 122 #else // not WIN32 123 wxMemoryDC thedc; 124 125 // fix fontsize to be 72 DPI 126 //fontsize = -FT_MulDiv((int)(fontsize+0.5), 72, thedc.GetPPI().y); 127 128 // now try to get a font! 129 // use the font list to get some caching... (chance is the script will need the same font very often) 130 // USING wxTheFontList SEEMS TO CAUSE BAD LEAKS! 131 //wxFont *thefont = wxTheFontList->FindOrCreateFont( 132 wxFont thefont( 133 (int)fontsize, 134 wxFONTFAMILY_DEFAULT, 135 style->italic ? wxFONTSTYLE_ITALIC : wxFONTSTYLE_NORMAL, 136 style->bold ? wxFONTWEIGHT_BOLD : wxFONTWEIGHT_NORMAL, 137 style->underline, 138 to_wx(style->font), 139 wxFONTENCODING_SYSTEM); // FIXME! make sure to get the right encoding here, make some translation table between windows and wx encodings 140 thedc.SetFont(thefont); 141 142 wxString wtext(to_wx(text)); 143 if (spacing) { 144 // If there's inter-character spacing, kerning info must not be used, so calculate width per character 145 // NOTE: Is kerning actually done either way?! 146 for (auto const& wc : wtext) { 147 int a, b, c, d; 148 thedc.GetTextExtent(wc, &a, &b, &c, &d); 149 double scaling = fontsize / (double)(b > 0 ? b : 1); // semi-workaround for missing OS/2 table data for scaling 150 width += (a + spacing)*scaling; 151 height = b > height ? b*scaling : height; 152 descent = c > descent ? c*scaling : descent; 153 extlead = d > extlead ? d*scaling : extlead; 154 } 155 } else { 156 // If the inter-character spacing should be zero, kerning info can (and must) be used, so calculate everything in one go 157 wxCoord lwidth, lheight, ldescent, lextlead; 158 thedc.GetTextExtent(wtext, &lwidth, &lheight, &ldescent, &lextlead); 159 double scaling = fontsize / (double)(lheight > 0 ? lheight : 1); // semi-workaround for missing OS/2 table data for scaling 160 width = lwidth*scaling; height = lheight*scaling; descent = ldescent*scaling; extlead = lextlead*scaling; 161 } 162 #endif 163 164 // Compensate for scaling 165 width = style->scalex / 100 * width / 64; 166 height = style->scaley / 100 * height / 64; 167 descent = style->scaley / 100 * descent / 64; 168 extlead = style->scaley / 100 * extlead / 64; 169 170 return true; 171 } 172 ExportFilter(std::string const & name,std::string const & description,int priority)173 ExportFilter::ExportFilter(std::string const& name, std::string const& description, int priority) 174 : AssExportFilter(name, description, priority) 175 { 176 } 177 GetScriptSettingsIdentifier()178 std::string ExportFilter::GetScriptSettingsIdentifier() 179 { 180 return inline_string_encode(GetName()); 181 } 182 GetConfigDialogWindow(wxWindow * parent,agi::Context * c)183 wxWindow* ExportFilter::GetConfigDialogWindow(wxWindow *parent, agi::Context *c) { 184 config_dialog = GenerateConfigDialog(parent, c); 185 186 if (config_dialog) { 187 std::string const& val = c->ass->Properties.automation_settings[GetScriptSettingsIdentifier()]; 188 if (!val.empty()) 189 config_dialog->Unserialise(val); 190 return config_dialog->CreateWindow(parent); 191 } 192 193 return nullptr; 194 } 195 LoadSettings(bool is_default,agi::Context * c)196 void ExportFilter::LoadSettings(bool is_default, agi::Context *c) { 197 if (config_dialog) 198 c->ass->Properties.automation_settings[GetScriptSettingsIdentifier()] = config_dialog->Serialise(); 199 } 200 201 // ProgressSink ProgressSink(agi::ProgressSink * impl,BackgroundScriptRunner * bsr)202 ProgressSink::ProgressSink(agi::ProgressSink *impl, BackgroundScriptRunner *bsr) 203 : impl(impl) 204 , bsr(bsr) 205 , trace_level(OPT_GET("Automation/Trace Level")->GetInt()) 206 { 207 } 208 ShowDialog(ScriptDialog * config_dialog)209 void ProgressSink::ShowDialog(ScriptDialog *config_dialog) 210 { 211 agi::dispatch::Main().Sync([=] { 212 wxDialog w; // container dialog box 213 w.SetExtraStyle(wxWS_EX_VALIDATE_RECURSIVELY); 214 w.Create(bsr->GetParentWindow(), -1, to_wx(bsr->GetTitle())); 215 auto s = new wxBoxSizer(wxHORIZONTAL); // sizer for putting contents in 216 wxWindow *ww = config_dialog->CreateWindow(&w); // generate actual dialog contents 217 s->Add(ww, 0, wxALL, 5); // add contents to dialog 218 w.SetSizerAndFit(s); 219 w.CenterOnParent(); 220 w.ShowModal(); 221 }); 222 } 223 ShowDialog(wxDialog * dialog)224 int ProgressSink::ShowDialog(wxDialog *dialog) 225 { 226 int ret = 0; 227 agi::dispatch::Main().Sync([&] { ret = dialog->ShowModal(); }); 228 return ret; 229 } 230 BackgroundScriptRunner(wxWindow * parent,std::string const & title)231 BackgroundScriptRunner::BackgroundScriptRunner(wxWindow *parent, std::string const& title) 232 : impl(new DialogProgress(parent, to_wx(title))) 233 { 234 } 235 ~BackgroundScriptRunner()236 BackgroundScriptRunner::~BackgroundScriptRunner() 237 { 238 } 239 Run(std::function<void (ProgressSink *)> task)240 void BackgroundScriptRunner::Run(std::function<void (ProgressSink*)> task) 241 { 242 impl->Run([&](agi::ProgressSink *ps) { 243 ProgressSink aps(ps, this); 244 task(&aps); 245 }); 246 } 247 GetParentWindow() const248 wxWindow *BackgroundScriptRunner::GetParentWindow() const 249 { 250 return impl.get(); 251 } 252 GetTitle() const253 std::string BackgroundScriptRunner::GetTitle() const 254 { 255 return from_wx(impl->GetTitle()); 256 } 257 258 // Script Script(agi::fs::path const & filename)259 Script::Script(agi::fs::path const& filename) 260 : filename(filename) 261 { 262 include_path.emplace_back(filename.parent_path()); 263 264 std::string include_paths = OPT_GET("Path/Automation/Include")->GetString(); 265 boost::char_separator<char> sep("|"); 266 for (auto const& tok : boost::tokenizer<boost::char_separator<char>>(include_paths, sep)) { 267 auto path = config::path->Decode(tok); 268 if (path.is_absolute() && agi::fs::DirectoryExists(path)) 269 include_path.emplace_back(std::move(path)); 270 } 271 } 272 273 // ScriptManager Add(std::unique_ptr<Script> script)274 void ScriptManager::Add(std::unique_ptr<Script> script) 275 { 276 if (find(scripts.begin(), scripts.end(), script) == scripts.end()) 277 scripts.emplace_back(std::move(script)); 278 279 ScriptsChanged(); 280 } 281 Remove(Script * script)282 void ScriptManager::Remove(Script *script) 283 { 284 auto i = find_if(scripts.begin(), scripts.end(), [&](std::unique_ptr<Script> const& s) { return s.get() == script; }); 285 if (i != scripts.end()) 286 scripts.erase(i); 287 288 ScriptsChanged(); 289 } 290 RemoveAll()291 void ScriptManager::RemoveAll() 292 { 293 scripts.clear(); 294 ScriptsChanged(); 295 } 296 Reload(Script * script)297 void ScriptManager::Reload(Script *script) 298 { 299 script->Reload(); 300 ScriptsChanged(); 301 } 302 GetMacros()303 const std::vector<cmd::Command*>& ScriptManager::GetMacros() 304 { 305 macros.clear(); 306 for (auto& script : scripts) { 307 std::vector<cmd::Command*> sfs = script->GetMacros(); 308 copy(sfs.begin(), sfs.end(), back_inserter(macros)); 309 } 310 return macros; 311 } 312 313 // AutoloadScriptManager AutoloadScriptManager(std::string path)314 AutoloadScriptManager::AutoloadScriptManager(std::string path) 315 : path(std::move(path)) 316 { 317 Reload(); 318 } 319 Reload()320 void AutoloadScriptManager::Reload() 321 { 322 scripts.clear(); 323 324 std::vector<std::future<std::unique_ptr<Script>>> script_futures; 325 326 boost::char_separator<char> sep("|"); 327 for (auto const& tok : boost::tokenizer<boost::char_separator<char>>(path, sep)) { 328 auto dirname = config::path->Decode(tok); 329 if (!agi::fs::DirectoryExists(dirname)) continue; 330 331 for (auto filename : agi::fs::DirectoryIterator(dirname, "*.*")) 332 script_futures.emplace_back(std::async(std::launch::async, [=] { 333 return ScriptFactory::CreateFromFile(dirname/filename, false, false); 334 })); 335 } 336 337 int error_count = 0; 338 for (auto& future : script_futures) { 339 auto s = future.get(); 340 if (s) { 341 if (!s->GetLoadedState()) ++error_count; 342 scripts.emplace_back(std::move(s)); 343 } 344 } 345 346 if (error_count == 1) { 347 wxLogWarning("A script in the Automation autoload directory failed to load.\nPlease review the errors, fix them and use the Rescan Autoload Dir button in Automation Manager to load the scripts again."); 348 } 349 else if (error_count > 1) { 350 wxLogWarning("Multiple scripts in the Automation autoload directory failed to load.\nPlease review the errors, fix them and use the Rescan Autoload Dir button in Automation Manager to load the scripts again."); 351 } 352 353 ScriptsChanged(); 354 } 355 LocalScriptManager(agi::Context * c)356 LocalScriptManager::LocalScriptManager(agi::Context *c) 357 : context(c) 358 , file_open_connection(c->subsController->AddFileOpenListener(&LocalScriptManager::Reload, this)) 359 { 360 AddScriptChangeListener(&LocalScriptManager::SaveLoadedList, this); 361 } 362 Reload()363 void LocalScriptManager::Reload() 364 { 365 scripts.clear(); 366 367 auto const& local_scripts = context->ass->Properties.automation_scripts; 368 if (local_scripts.empty()) { 369 ScriptsChanged(); 370 return; 371 } 372 373 auto autobasefn(OPT_GET("Path/Automation/Base")->GetString()); 374 375 boost::char_separator<char> sep("|"); 376 for (auto const& cur : boost::tokenizer<boost::char_separator<char>>(local_scripts, sep)) { 377 auto trimmed = boost::trim_copy(cur); 378 char first_char = trimmed[0]; 379 trimmed.erase(0, 1); 380 381 agi::fs::path basepath; 382 if (first_char == '~') { 383 basepath = context->subsController->Filename().parent_path(); 384 } else if (first_char == '$') { 385 basepath = autobasefn; 386 } else if (first_char == '/') { 387 } else { 388 wxLogWarning("Automation Script referenced with unknown location specifier character.\nLocation specifier found: %c\nFilename specified: %s", 389 first_char, to_wx(trimmed)); 390 continue; 391 } 392 auto sfname = basepath/trimmed; 393 if (agi::fs::FileExists(sfname)) 394 scripts.emplace_back(Automation4::ScriptFactory::CreateFromFile(sfname, true)); 395 else { 396 wxLogWarning("Automation Script referenced could not be found.\nFilename specified: %c%s\nSearched relative to: %s\nResolved filename: %s", 397 first_char, to_wx(trimmed), basepath.wstring(), sfname.wstring()); 398 } 399 } 400 401 ScriptsChanged(); 402 } 403 SaveLoadedList()404 void LocalScriptManager::SaveLoadedList() 405 { 406 // Store Automation script data 407 // Algorithm: 408 // 1. If script filename has Automation Base Path as a prefix, the path is relative to that (ie. "$") 409 // 2. Otherwise try making it relative to the ass filename 410 // 3. If step 2 failed, or absolute path is shorter than path relative to ass, use absolute path ("/") 411 // 4. Otherwise, use path relative to ass ("~") 412 std::string scripts_string; 413 agi::fs::path autobasefn(OPT_GET("Path/Automation/Base")->GetString()); 414 415 for (auto& script : GetScripts()) { 416 if (!scripts_string.empty()) 417 scripts_string += "|"; 418 419 auto scriptfn(script->GetFilename().string()); 420 auto autobase_rel = config::path->MakeRelative(scriptfn, autobasefn); 421 auto assfile_rel = config::path->MakeRelative(scriptfn, "?script"); 422 423 if (autobase_rel.string().size() <= scriptfn.size() && autobase_rel.string().size() <= assfile_rel.string().size()) { 424 scriptfn = "$" + autobase_rel.generic_string(); 425 } else if (assfile_rel.string().size() <= scriptfn.size() && assfile_rel.string().size() <= autobase_rel.string().size()) { 426 scriptfn = "~" + assfile_rel.generic_string(); 427 } else { 428 scriptfn = "/" + script->GetFilename().generic_string(); 429 } 430 431 scripts_string += scriptfn; 432 } 433 context->ass->Properties.automation_scripts = std::move(scripts_string); 434 } 435 436 // ScriptFactory ScriptFactory(std::string engine_name,std::string filename_pattern)437 ScriptFactory::ScriptFactory(std::string engine_name, std::string filename_pattern) 438 : engine_name(std::move(engine_name)) 439 , filename_pattern(std::move(filename_pattern)) 440 { 441 } 442 Register(std::unique_ptr<ScriptFactory> factory)443 void ScriptFactory::Register(std::unique_ptr<ScriptFactory> factory) 444 { 445 if (find(Factories().begin(), Factories().end(), factory) != Factories().end()) 446 throw agi::InternalError("Automation 4: Attempt to register the same script factory multiple times. This should never happen."); 447 448 Factories().emplace_back(std::move(factory)); 449 } 450 CreateFromFile(agi::fs::path const & filename,bool complain_about_unrecognised,bool create_unknown)451 std::unique_ptr<Script> ScriptFactory::CreateFromFile(agi::fs::path const& filename, bool complain_about_unrecognised, bool create_unknown) 452 { 453 for (auto& factory : Factories()) { 454 auto s = factory->Produce(filename); 455 if (s) { 456 if (!s->GetLoadedState()) { 457 wxLogError(_("Failed to load Automation script '%s':\n%s"), filename.wstring(), s->GetDescription()); 458 } 459 return s; 460 } 461 } 462 463 if (complain_about_unrecognised) { 464 wxLogError(_("The file was not recognised as an Automation script: %s"), filename.wstring()); 465 } 466 467 return create_unknown ? agi::make_unique<UnknownScript>(filename) : nullptr; 468 } 469 Factories()470 std::vector<std::unique_ptr<ScriptFactory>>& ScriptFactory::Factories() 471 { 472 static std::vector<std::unique_ptr<ScriptFactory>> factories; 473 return factories; 474 } 475 GetFactories()476 const std::vector<std::unique_ptr<ScriptFactory>>& ScriptFactory::GetFactories() 477 { 478 return Factories(); 479 } 480 GetWildcardStr()481 std::string ScriptFactory::GetWildcardStr() 482 { 483 std::string fnfilter, catchall; 484 for (auto& fact : Factories()) { 485 if (fact->GetEngineName().empty() || fact->GetFilenamePattern().empty()) 486 continue; 487 488 std::string filter(fact->GetFilenamePattern()); 489 boost::replace_all(filter, ",", ";"); 490 fnfilter += agi::format("%s scripts (%s)|%s|", fact->GetEngineName(), fact->GetFilenamePattern(), filter); 491 catchall += filter + ";"; 492 } 493 fnfilter += from_wx(_("All Files")) + " (*.*)|*.*"; 494 495 if (!catchall.empty()) 496 catchall.pop_back(); 497 498 if (Factories().size() > 1) 499 fnfilter = from_wx(_("All Supported Formats")) + "|" + catchall + "|" + fnfilter; 500 501 return fnfilter; 502 } 503 GetDescription() const504 std::string UnknownScript::GetDescription() const { 505 return from_wx(_("File was not recognized as a script")); 506 } 507 } 508