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