// Copyright (c) 2005, Rodrigo Braz Monteiro // 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 Project http://www.aegisub.org/ /// @file ass_file.cpp /// @brief Overall storage of subtitle files, undo management and more /// @ingroup subs_storage #include "config.h" #ifndef AGI_PRE #include #include #include #include #include #include #include #endif #include "ass_attachment.h" #include "ass_dialogue.h" #include "ass_file.h" #include "ass_override.h" #include "ass_style.h" #include "compat.h" #include "main.h" #include "standard_paths.h" #include "subtitle_format.h" #include "text_file_reader.h" #include "text_file_writer.h" #include "utils.h" namespace std { template<> void swap(AssFile &lft, AssFile &rgt) { lft.swap(rgt); } } AssFile::AssFile () : commitId(0) , loaded(false) { } AssFile::~AssFile() { background_delete_clear(Line); } /// @brief Load generic subs void AssFile::Load(const wxString &_filename, wxString const& charset) { const SubtitleFormat *reader = SubtitleFormat::GetReader(_filename); try { AssFile temp; reader->ReadFile(&temp, _filename, charset); bool found_style = false; bool found_dialogue = false; // Check if the file has at least one style and at least one dialogue line for (entryIter it = temp.Line.begin(); it != temp.Line.end(); ++it) { AssEntryType type = (*it)->GetType(); if (type == ENTRY_STYLE) found_style = true; if (type == ENTRY_DIALOGUE) found_dialogue = true; if (found_style && found_dialogue) break; } // And if it doesn't add defaults for each if (!found_style) temp.InsertStyle(new AssStyle); if (!found_dialogue) temp.InsertDialogue(new AssDialogue); swap(temp); } catch (agi::UserCancelException const&) { return; } // Set general data loaded = true; filename = _filename; // Add comments and set vars SetScriptInfo("ScriptType", "v4.00+"); // Push the initial state of the file onto the undo stack UndoStack.clear(); RedoStack.clear(); undoDescription.clear(); autosavedCommitId = savedCommitId = commitId + 1; Commit("", COMMIT_NEW); FileOpen(filename); } void AssFile::Save(wxString filename, bool setfilename, bool addToRecent, wxString encoding) { const SubtitleFormat *writer = SubtitleFormat::GetWriter(filename); if (!writer) throw "Unknown file type."; if (setfilename) { autosavedCommitId = savedCommitId = commitId; this->filename = filename; StandardPaths::SetPathValue("?script", wxFileName(filename).GetPath()); } FileSave(); writer->WriteFile(this, filename, encoding); if (addToRecent) { AddToRecent(filename); } } wxString AssFile::AutoSave() { if (!loaded || commitId == autosavedCommitId) return ""; wxFileName origfile(filename); wxString path = lagi_wxString(OPT_GET("Path/Auto/Save")->GetString()); if (!path) path = origfile.GetPath(); path = StandardPaths::DecodePath(path + "/"); wxFileName dstpath(path); if (!dstpath.DirExists()) wxMkdir(path); wxString name = origfile.GetName(); if (!name) name = "Untitled"; dstpath.SetFullName(wxString::Format("%s.%s.AUTOSAVE.ass", name, wxDateTime::Now().Format("%Y-%m-%d-%H-%M-%S"))); Save(dstpath.GetFullPath(), false, false); autosavedCommitId = commitId; return dstpath.GetFullPath(); } void AssFile::SaveMemory(std::vector &dst) { // Check if subs contain at least one style // Add a default style if they don't for compatibility with libass/asa if (GetStyles().Count() == 0) InsertStyle(new AssStyle); // Prepare vector dst.clear(); dst.reserve(0x4000); // Write file for (entryIter cur = Line.begin(); cur != Line.end(); ++cur) { wxCharBuffer buffer = ((*cur)->GetEntryData() + "\r\n").utf8_str(); copy(buffer.data(), buffer.data() + buffer.length(), back_inserter(dst)); } } bool AssFile::CanSave() const { try { return SubtitleFormat::GetWriter(filename)->CanSave(this); } catch (...) { return false; } } void AssFile::Clear() { background_delete_clear(Line); loaded = false; filename.clear(); UndoStack.clear(); RedoStack.clear(); undoDescription.clear(); } void AssFile::LoadDefault(bool defline) { Clear(); // Write headers Line.push_back(new AssEntry("[Script Info]", "[Script Info]")); Line.push_back(new AssEntry("Title: Default Aegisub file", "[Script Info]")); Line.push_back(new AssEntry("ScriptType: v4.00+", "[Script Info]")); Line.push_back(new AssEntry("WrapStyle: 0", "[Script Info]")); Line.push_back(new AssEntry("ScaledBorderAndShadow: yes", "[Script Info]")); Line.push_back(new AssEntry("Collisions: Normal", "[Script Info]")); if (!OPT_GET("Subtitle/Default Resolution/Auto")->GetBool()) { Line.push_back(new AssEntry(wxString::Format("PlayResX: %" PRId64, OPT_GET("Subtitle/Default Resolution/Width")->GetInt()), "[Script Info]")); Line.push_back(new AssEntry(wxString::Format("PlayResY: %" PRId64, OPT_GET("Subtitle/Default Resolution/Height")->GetInt()), "[Script Info]")); } Line.push_back(new AssEntry("YCbCr Matrix: None", "[Script Info]")); InsertStyle(new AssStyle); Line.push_back(new AssEntry("[Events]", "[Events]")); Line.push_back(new AssEntry("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text", "[Events]")); if (defline) Line.push_back(new AssDialogue); autosavedCommitId = savedCommitId = commitId + 1; Commit("", COMMIT_NEW); loaded = true; StandardPaths::SetPathValue("?script", ""); FileOpen(""); } void AssFile::swap(AssFile &that) throw() { // Intentionally does not swap undo stack related things std::swap(loaded, that.loaded); std::swap(commitId, that.commitId); std::swap(undoDescription, that.undoDescription); std::swap(Line, that.Line); } AssFile::AssFile(const AssFile &from) : undoDescription(from.undoDescription) , commitId(from.commitId) , filename(from.filename) , loaded(from.loaded) { std::transform(from.Line.begin(), from.Line.end(), std::back_inserter(Line), std::mem_fun(&AssEntry::Clone)); } AssFile& AssFile::operator=(AssFile from) { std::swap(*this, from); return *this; } static bool try_insert(std::list &lines, AssEntry *entry) { if (lines.empty()) return false; // Search for insertion point std::list::iterator it = lines.end(); do { --it; if ((*it)->group == entry->group) { lines.insert(++it, entry); return true; } } while (it != lines.begin()); return false; } void AssFile::InsertStyle(AssStyle *style) { if (try_insert(Line, style)) return; // No styles found, add them Line.push_back(new AssEntry("[V4+ Styles]", "[V4+ Styles]")); Line.push_back(new AssEntry("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding", "[V4+ Styles]")); Line.push_back(style); } void AssFile::InsertAttachment(AssAttachment *attach) { if (try_insert(Line, attach)) return; // Didn't find a group of the appropriate type so create it Line.push_back(new AssEntry(attach->group, attach->group)); Line.push_back(attach); } void AssFile::InsertAttachment(wxString filename) { wxString group("[Graphics]"); wxString ext = filename.Right(4).Lower(); if (ext == ".ttf" || ext == ".ttc" || ext == ".pfb") group = "[Fonts]"; std::auto_ptr newAttach(new AssAttachment(wxFileName(filename).GetFullName(), group)); newAttach->Import(filename); InsertAttachment(newAttach.release()); } void AssFile::InsertDialogue(AssDialogue *diag) { if (try_insert(Line, diag)) return; // Didn't find a group of the appropriate type so create it Line.push_back(new AssEntry("[Events]", "[Events]")); Line.push_back(new AssEntry("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text", "[Events]")); Line.push_back(diag); } wxString AssFile::GetScriptInfo(wxString key) const { key.MakeLower(); key += ":"; bool GotIn = false; for (std::list::const_iterator cur = Line.begin(); cur != Line.end(); ++cur) { if ((*cur)->group == "[Script Info]") { GotIn = true; wxString curText = (*cur)->GetEntryData(); if (curText.Lower().StartsWith(key)) return curText.Mid(key.size()).Trim(true).Trim(false); } else if (GotIn) return ""; } return ""; } int AssFile::GetScriptInfoAsInt(wxString const& key) const { long temp = 0; GetScriptInfo(key).ToLong(&temp); return temp; } void AssFile::SetScriptInfo(wxString const& key, wxString const& value) { wxString search_key = key.Lower() + ":"; size_t key_size = search_key.size(); std::list::iterator script_info_end; bool found_script_info = false; for (std::list::iterator cur = Line.begin(); cur != Line.end(); ++cur) { if ((*cur)->group == "[Script Info]") { found_script_info = true; wxString cur_text = (*cur)->GetEntryData().Left(key_size).Lower(); if (cur_text == search_key) { if (value.empty()) { delete *cur; Line.erase(cur); } else { (*cur)->SetEntryData(key + ": " + value); } return; } script_info_end = cur; } else if (found_script_info) { if (value.size()) Line.insert(script_info_end, new AssEntry(key + ": " + value, "[Script Info]")); return; } } // Found a script info section, but not this key or anything after it, // so add it at the end of the file if (found_script_info) Line.push_back(new AssEntry(key + ": " + value, "[Script Info]")); // Script info section not found, so add it at the beginning of the file else { Line.push_front(new AssEntry(key + ": " + value, "[Script Info]")); Line.push_front(new AssEntry("[Script Info]", "[Script Info]")); } } void AssFile::GetResolution(int &sw,int &sh) const { sw = GetScriptInfoAsInt("PlayResX"); sh = GetScriptInfoAsInt("PlayResY"); // Gabest logic? if (sw == 0 && sh == 0) { sw = 384; sh = 288; } else if (sw == 0) { if (sh == 1024) sw = 1280; else sw = sh * 4 / 3; } else if (sh == 0) { // you are not crazy; this doesn't make any sense if (sw == 1280) sh = 1024; else sh = sw * 3 / 4; } } wxArrayString AssFile::GetStyles() const { wxArrayString styles; for (std::list::const_iterator cur = Line.begin(); cur != Line.end(); ++cur) { if (AssStyle *curstyle = dynamic_cast(*cur)) styles.Add(curstyle->name); } return styles; } AssStyle *AssFile::GetStyle(wxString const& name) { for (entryIter cur = Line.begin(); cur != Line.end(); ++cur) { AssStyle *curstyle = dynamic_cast(*cur); if (curstyle && curstyle->name == name) return curstyle; } return NULL; } void AssFile::AddToRecent(wxString const& file) const { config::mru->Add("Subtitle", STD_STR(file)); wxFileName filepath(file); OPT_SET("Path/Last/Subtitles")->SetString(STD_STR(filepath.GetPath())); } int AssFile::Commit(wxString const& desc, int type, int amendId, AssEntry *single_line) { ++commitId; // Allow coalescing only if it's the last change and the file has not been // saved since the last change if (commitId == amendId+1 && RedoStack.empty() && savedCommitId+1 != commitId && autosavedCommitId+1 != commitId) { // If only one line changed just modify it instead of copying the file if (single_line) { entryIter this_it = Line.begin(), undo_it = UndoStack.back().Line.begin(); while (*this_it != single_line) { ++this_it; ++undo_it; } delete *undo_it; *undo_it = single_line->Clone(); } else { UndoStack.back() = *this; } AnnounceCommit(type); return commitId; } RedoStack.clear(); // Place copy on stack undoDescription = desc; UndoStack.push_back(*this); // Cap depth int depth = std::max(OPT_GET("Limits/Undo Levels")->GetInt(), 2); while ((int)UndoStack.size() > depth) { UndoStack.pop_front(); } if (UndoStack.size() > 1 && OPT_GET("App/Auto/Save on Every Change")->GetBool() && !filename.empty() && CanSave()) Save(filename); AnnounceCommit(type); return commitId; } void AssFile::Undo() { if (UndoStack.size() <= 1) return; RedoStack.push_back(AssFile()); std::swap(RedoStack.back(), *this); UndoStack.pop_back(); *this = UndoStack.back(); AnnounceCommit(COMMIT_NEW); } void AssFile::Redo() { if (RedoStack.empty()) return; std::swap(*this, RedoStack.back()); UndoStack.push_back(*this); RedoStack.pop_back(); AnnounceCommit(COMMIT_NEW); } wxString AssFile::GetUndoDescription() const { return IsUndoStackEmpty() ? "" : UndoStack.back().undoDescription; } wxString AssFile::GetRedoDescription() const { return IsRedoStackEmpty() ? "" : RedoStack.back().undoDescription; } bool AssFile::CompStart(const AssDialogue* lft, const AssDialogue* rgt) { return lft->Start < rgt->Start; } bool AssFile::CompEnd(const AssDialogue* lft, const AssDialogue* rgt) { return lft->End < rgt->End; } bool AssFile::CompStyle(const AssDialogue* lft, const AssDialogue* rgt) { return lft->Style < rgt->Style; } bool AssFile::CompActor(const AssDialogue* lft, const AssDialogue* rgt) { return lft->Actor < rgt->Actor; } bool AssFile::CompEffect(const AssDialogue* lft, const AssDialogue* rgt) { return lft->Effect < rgt->Effect; } bool AssFile::CompLayer(const AssDialogue* lft, const AssDialogue* rgt) { return lft->Layer < rgt->Layer; } void AssFile::Sort(CompFunc comp, std::set const& limit) { Sort(Line, comp, limit); } namespace { struct AssEntryComp : public std::binary_function { AssFile::CompFunc comp; bool operator()(const AssEntry* a, const AssEntry* b) const { return comp(static_cast(a), static_cast(b)); } }; inline bool is_dialogue(AssEntry *e, std::set const& limit) { AssDialogue *d = dynamic_cast(e); return d && (limit.empty() || limit.count(d)); } } void AssFile::Sort(std::list &lst, CompFunc comp, std::set const& limit) { AssEntryComp compE; compE.comp = comp; // Sort each block of AssDialogues separately, leaving everything else untouched for (entryIter begin = lst.begin(); begin != lst.end(); ++begin) { if (!is_dialogue(*begin, limit)) continue; entryIter end = begin; while (end != lst.end() && is_dialogue(*end, limit)) ++end; // used instead of std::list::sort for partial list sorting std::list tmp; tmp.splice(tmp.begin(), lst, begin, end); tmp.sort(compE); lst.splice(end, tmp); begin = --end; } } void AssFile::Sort(std::list &lst, CompFunc comp) { lst.sort(comp); } AssFile *AssFile::top;