forked from mia/Aegisub
4ec507f814
Document all of the SubtitleFormat methods. Add default implementations of CanReadFile and CanWriteFile that check against the appropriate wildcard list. Clean up and simplify a lot of very odd code. Throw typed exceptions in all subtitle readers rather than strings. Originally committed to SVN as r5617.
404 lines
12 KiB
C++
404 lines
12 KiB
C++
// Copyright (c) 2006, 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/
|
|
//
|
|
// $Id$
|
|
|
|
/// @file subtitle_format.cpp
|
|
/// @brief Base class for subtitle format handlers
|
|
/// @ingroup subtitle_io
|
|
///
|
|
|
|
#include "config.h"
|
|
|
|
#ifndef AGI_PRE
|
|
#include <wx/intl.h>
|
|
#include <wx/choicdlg.h> // Keep this last so wxUSE_CHOICEDLG is set.
|
|
#endif
|
|
|
|
#include "ass_dialogue.h"
|
|
#include "ass_file.h"
|
|
#include "subtitle_format.h"
|
|
#include "subtitle_format_ass.h"
|
|
#include "subtitle_format_dvd.h"
|
|
#include "subtitle_format_encore.h"
|
|
#include "subtitle_format_microdvd.h"
|
|
#include "subtitle_format_mkv.h"
|
|
#include "subtitle_format_srt.h"
|
|
#include "subtitle_format_transtation.h"
|
|
#include "subtitle_format_ttxt.h"
|
|
#include "subtitle_format_txt.h"
|
|
#include "utils.h"
|
|
#include "video_context.h"
|
|
|
|
using namespace std::tr1::placeholders;
|
|
|
|
SubtitleFormat::SubtitleFormat(wxString const& name)
|
|
: name(name)
|
|
, isCopy(0)
|
|
, Line(0)
|
|
{
|
|
formats.push_back(this);
|
|
}
|
|
|
|
SubtitleFormat::~SubtitleFormat() {
|
|
formats.remove(this);
|
|
}
|
|
|
|
void SubtitleFormat::SetTarget(AssFile *file) {
|
|
ClearCopy();
|
|
Line = file ? &file->Line : 0;
|
|
assFile = file;
|
|
}
|
|
|
|
bool SubtitleFormat::CanReadFile(wxString const& filename) const {
|
|
return GetReadWildcards().Index(filename.AfterLast('.'), false) != wxNOT_FOUND;
|
|
}
|
|
|
|
bool SubtitleFormat::CanWriteFile(wxString const& filename) const {
|
|
return GetWriteWildcards().Index(filename.AfterLast('.'), false) != wxNOT_FOUND;
|
|
}
|
|
|
|
void SubtitleFormat::CreateCopy() {
|
|
SetTarget(new AssFile(*assFile));
|
|
isCopy = true;
|
|
}
|
|
|
|
void SubtitleFormat::ClearCopy() {
|
|
if (isCopy) {
|
|
delete assFile;
|
|
assFile = NULL;
|
|
isCopy = false;
|
|
}
|
|
}
|
|
|
|
void SubtitleFormat::Clear() {
|
|
assFile->Clear();
|
|
}
|
|
|
|
void SubtitleFormat::LoadDefault(bool defline) {
|
|
assFile->LoadDefault(defline);
|
|
}
|
|
|
|
void SubtitleFormat::AddLine(wxString data, wxString group, int &version, wxString *outgroup) {
|
|
assFile->AddLine(data, group, version, outgroup);
|
|
}
|
|
|
|
/// @brief Ask the user to enter the FPS
|
|
SubtitleFormat::FPSRational SubtitleFormat::AskForFPS(bool showSMPTE) {
|
|
wxArrayString choices;
|
|
FPSRational fps_rat;
|
|
fps_rat.smpte_dropframe = false; // ensure it's false by default
|
|
|
|
// Video FPS
|
|
VideoContext *context = VideoContext::Get();
|
|
bool vidLoaded = context->TimecodesLoaded();
|
|
if (vidLoaded) {
|
|
wxString vidFPS;
|
|
if (context->FPS().IsVFR()) vidFPS = "VFR";
|
|
else vidFPS = wxString::Format("%.3f", context->FPS().FPS());
|
|
choices.Add(wxString::Format("From video (%s)", vidFPS));
|
|
}
|
|
|
|
// Standard FPS values
|
|
choices.Add(_("15.000 FPS"));
|
|
choices.Add(_("23.976 FPS (Decimated NTSC)"));
|
|
choices.Add(_("24.000 FPS (FILM)"));
|
|
choices.Add(_("25.000 FPS (PAL)"));
|
|
choices.Add(_("29.970 FPS (NTSC)"));
|
|
if (showSMPTE)
|
|
choices.Add(_("29.970 FPS (NTSC with SMPTE dropframe)"));
|
|
choices.Add(_("30.000 FPS"));
|
|
choices.Add(_("50.000 FPS (PAL x2)"));
|
|
choices.Add(_("59.940 FPS (NTSC x2)"));
|
|
choices.Add(_("60.000 FPS"));
|
|
choices.Add(_("119.880 FPS (NTSC x4)"));
|
|
choices.Add(_("120.000 FPS"));
|
|
|
|
// Ask
|
|
int choice = wxGetSingleChoiceIndex(_("Please choose the appropriate FPS for the subtitles:"), _("FPS"), choices);
|
|
if (choice == -1) {
|
|
fps_rat.num = 0;
|
|
fps_rat.den = 0;
|
|
|
|
return fps_rat;
|
|
}
|
|
|
|
// Get FPS from choice
|
|
if (vidLoaded) choice--;
|
|
// dropframe was displayed, that means all choices >4 are bumped up by 1
|
|
if (showSMPTE) {
|
|
switch (choice) {
|
|
case -1: fps_rat.num = -1; fps_rat.den = 1; break; // VIDEO
|
|
case 0: fps_rat.num = 15; fps_rat.den = 1; break;
|
|
case 1: fps_rat.num = 24000; fps_rat.den = 1001; break;
|
|
case 2: fps_rat.num = 24; fps_rat.den = 1; break;
|
|
case 3: fps_rat.num = 25; fps_rat.den = 1; break;
|
|
case 4: fps_rat.num = 30000; fps_rat.den = 1001; break;
|
|
case 5: fps_rat.num = 30000; fps_rat.den = 1001; fps_rat.smpte_dropframe = true; break;
|
|
case 6: fps_rat.num = 30; fps_rat.den = 1; break;
|
|
case 7: fps_rat.num = 50; fps_rat.den = 1; break;
|
|
case 8: fps_rat.num = 60000; fps_rat.den = 1001; break;
|
|
case 9: fps_rat.num = 60; fps_rat.den = 1; break;
|
|
case 10: fps_rat.num = 120000; fps_rat.den = 1001; break;
|
|
case 11: fps_rat.num = 120; fps_rat.den = 1; break;
|
|
}
|
|
return fps_rat;
|
|
} else {
|
|
// dropframe wasn't displayed
|
|
switch (choice) {
|
|
case -1: fps_rat.num = -1; fps_rat.den = 1; break; // VIDEO
|
|
case 0: fps_rat.num = 15; fps_rat.den = 1; break;
|
|
case 1: fps_rat.num = 24000; fps_rat.den = 1001; break;
|
|
case 2: fps_rat.num = 24; fps_rat.den = 1; break;
|
|
case 3: fps_rat.num = 25; fps_rat.den = 1; break;
|
|
case 4: fps_rat.num = 30000; fps_rat.den = 1001; break;
|
|
case 5: fps_rat.num = 30; fps_rat.den = 1; break;
|
|
case 6: fps_rat.num = 50; fps_rat.den = 1; break;
|
|
case 7: fps_rat.num = 60000; fps_rat.den = 1001; break;
|
|
case 8: fps_rat.num = 60; fps_rat.den = 1; break;
|
|
case 9: fps_rat.num = 120000; fps_rat.den = 1001; break;
|
|
case 10: fps_rat.num = 120; fps_rat.den = 1; break;
|
|
}
|
|
return fps_rat;
|
|
}
|
|
|
|
// fubar
|
|
fps_rat.num = 0;
|
|
fps_rat.den = 0;
|
|
|
|
return fps_rat;
|
|
}
|
|
|
|
void SubtitleFormat::SortLines() {
|
|
AssFile::Sort(*Line);
|
|
}
|
|
|
|
void SubtitleFormat::ConvertTags(int format, const wxString &lineEnd, bool mergeLineBreaks) {
|
|
for (std::list<AssEntry*>::iterator cur = Line->begin(); cur != Line->end(); ++cur) {
|
|
if (AssDialogue *current = dynamic_cast<AssDialogue*>(*cur)) {
|
|
// Strip tags
|
|
if (format == 1) current->StripTags();
|
|
else if (format == 2) current->ConvertTagsToSRT();
|
|
|
|
// Replace line breaks
|
|
current->Text.Replace("\\h", " ");
|
|
current->Text.Replace("\\n", lineEnd);
|
|
current->Text.Replace("\\N", lineEnd);
|
|
if (mergeLineBreaks) {
|
|
while (current->Text.Replace(lineEnd+lineEnd, lineEnd));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void SubtitleFormat::StripComments() {
|
|
for (std::list<AssEntry*>::iterator it = Line->begin(); it != Line->end(); ) {
|
|
AssDialogue *diag = dynamic_cast<AssDialogue*>(*it);
|
|
if (!diag || (!diag->Comment && diag->Text.size()))
|
|
++it;
|
|
else {
|
|
delete *it;
|
|
Line->erase(it++);
|
|
}
|
|
}
|
|
}
|
|
|
|
void SubtitleFormat::StripNonDialogue() {
|
|
for (std::list<AssEntry*>::iterator it = Line->begin(); it != Line->end(); ) {
|
|
if (dynamic_cast<AssDialogue*>(*it))
|
|
++it;
|
|
else {
|
|
delete *it;
|
|
Line->erase(it++);
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool dialog_start_lt(AssEntry *pos, AssDialogue *to_insert) {
|
|
AssDialogue *diag = dynamic_cast<AssDialogue*>(pos);
|
|
return diag && diag->Start > to_insert->Start;
|
|
}
|
|
|
|
/// @brief Split and merge lines so there are no overlapping lines
|
|
///
|
|
/// Algorithm described at http://devel.aegisub.org/wiki/Technical/SplitMerge
|
|
void SubtitleFormat::RecombineOverlaps() {
|
|
std::list<AssEntry*>::iterator cur, next = Line->begin();
|
|
cur = next++;
|
|
|
|
for (; next != Line->end(); cur = next++) {
|
|
AssDialogue *prevdlg = dynamic_cast<AssDialogue*>(*cur);
|
|
AssDialogue *curdlg = dynamic_cast<AssDialogue*>(*next);
|
|
|
|
if (!curdlg || !prevdlg) continue;
|
|
if (prevdlg->End <= curdlg->Start) continue;
|
|
|
|
// Use names like in the algorithm description and prepare for erasing
|
|
// old dialogues from the list
|
|
std::list<AssEntry*>::iterator prev = cur;
|
|
cur = next;
|
|
next++;
|
|
|
|
// std::list::insert() inserts items before the given iterator, so
|
|
// we need 'next' for inserting. 'prev' and 'cur' can safely be erased
|
|
// from the list now.
|
|
Line->erase(prev);
|
|
Line->erase(cur);
|
|
|
|
//Is there an A part before the overlap?
|
|
if (curdlg->Start > prevdlg->Start) {
|
|
// Produce new entry with correct values
|
|
AssDialogue *newdlg = dynamic_cast<AssDialogue*>(prevdlg->Clone());
|
|
newdlg->Start = prevdlg->Start;
|
|
newdlg->End = curdlg->Start;
|
|
newdlg->Text = prevdlg->Text;
|
|
|
|
Line->insert(find_if(next, Line->end(), bind(dialog_start_lt, _1, newdlg)), newdlg);
|
|
}
|
|
|
|
// Overlapping A+B part
|
|
{
|
|
AssDialogue *newdlg = dynamic_cast<AssDialogue*>(prevdlg->Clone());
|
|
newdlg->Start = curdlg->Start;
|
|
newdlg->End = (prevdlg->End < curdlg->End) ? prevdlg->End : curdlg->End;
|
|
// Put an ASS format hard linewrap between lines
|
|
newdlg->Text = curdlg->Text + "\\N" + prevdlg->Text;
|
|
|
|
Line->insert(find_if(next, Line->end(), bind(dialog_start_lt, _1, newdlg)), newdlg);
|
|
}
|
|
|
|
// Is there an A part after the overlap?
|
|
if (prevdlg->End > curdlg->End) {
|
|
// Produce new entry with correct values
|
|
AssDialogue *newdlg = dynamic_cast<AssDialogue*>(prevdlg->Clone());
|
|
newdlg->Start = curdlg->End;
|
|
newdlg->End = prevdlg->End;
|
|
newdlg->Text = prevdlg->Text;
|
|
|
|
Line->insert(find_if(next, Line->end(), bind(dialog_start_lt, _1, newdlg)), newdlg);
|
|
}
|
|
|
|
// Is there a B part after the overlap?
|
|
if (curdlg->End > prevdlg->End) {
|
|
// Produce new entry with correct values
|
|
AssDialogue *newdlg = dynamic_cast<AssDialogue*>(prevdlg->Clone());
|
|
newdlg->Start = prevdlg->End;
|
|
newdlg->End = curdlg->End;
|
|
newdlg->Text = curdlg->Text;
|
|
|
|
Line->insert(find_if(next, Line->end(), bind(dialog_start_lt, _1, newdlg)), newdlg);
|
|
}
|
|
|
|
next--;
|
|
}
|
|
}
|
|
|
|
/// @brief Merge identical lines that follow each other
|
|
void SubtitleFormat::MergeIdentical() {
|
|
std::list<AssEntry*>::iterator cur, next = Line->begin();
|
|
cur = next++;
|
|
|
|
for (; next != Line->end(); cur = next++) {
|
|
AssDialogue *curdlg = dynamic_cast<AssDialogue*>(*cur);
|
|
AssDialogue *nextdlg = dynamic_cast<AssDialogue*>(*next);
|
|
|
|
if (curdlg && nextdlg && curdlg->End == nextdlg->Start && curdlg->Text == nextdlg->Text) {
|
|
// Merge timing
|
|
nextdlg->Start = std::min(nextdlg->Start, curdlg->Start);
|
|
nextdlg->End = std::max(nextdlg->End, curdlg->End);
|
|
|
|
// Remove duplicate line
|
|
delete *cur;
|
|
Line->erase(cur);
|
|
}
|
|
}
|
|
}
|
|
|
|
std::list<SubtitleFormat*> SubtitleFormat::formats;
|
|
|
|
void SubtitleFormat::LoadFormats() {
|
|
if (formats.empty()) {
|
|
new ASSSubtitleFormat();
|
|
new EncoreSubtitleFormat();
|
|
new MKVSubtitleFormat();
|
|
new MicroDVDSubtitleFormat();
|
|
new SRTSubtitleFormat();
|
|
new TTXTSubtitleFormat();
|
|
new TXTSubtitleFormat();
|
|
new TranStationSubtitleFormat();
|
|
#ifdef _DEBUG
|
|
new DVDSubtitleFormat();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void SubtitleFormat::DestroyFormats() {
|
|
for (std::list<SubtitleFormat*>::iterator it = formats.begin(); it != formats.end(); )
|
|
delete *it++;
|
|
}
|
|
|
|
template<class Cont, class Pred>
|
|
SubtitleFormat *find_or_null(Cont &container, Pred pred) {
|
|
typename Cont::iterator it = find_if(container.begin(), container.end(), pred);
|
|
if (it == container.end())
|
|
return 0;
|
|
return *it;
|
|
}
|
|
|
|
SubtitleFormat *SubtitleFormat::GetReader(wxString const& filename) {
|
|
LoadFormats();
|
|
return find_or_null(formats, bind(&SubtitleFormat::CanReadFile, _1, filename));
|
|
}
|
|
|
|
SubtitleFormat *SubtitleFormat::GetWriter(wxString const& filename) {
|
|
LoadFormats();
|
|
return find_or_null(formats, bind(&SubtitleFormat::CanWriteFile, _1, filename));
|
|
}
|
|
|
|
wxString SubtitleFormat::GetWildcards(int mode) {
|
|
LoadFormats();
|
|
|
|
wxArrayString all;
|
|
wxString final;
|
|
|
|
std::list<SubtitleFormat*>::iterator curIter;
|
|
for (curIter=formats.begin();curIter!=formats.end();curIter++) {
|
|
SubtitleFormat *format = *curIter;
|
|
wxArrayString cur = mode == 0 ? format->GetReadWildcards() : format->GetWriteWildcards();
|
|
if (cur.empty()) continue;
|
|
|
|
for_each(cur.begin(), cur.end(), bind(&wxString::Prepend, _1, "*."));
|
|
copy(cur.begin(), cur.end(), std::back_inserter(all));
|
|
final += "|" + format->GetName() + " (" + wxJoin(cur, ',') + ")|" + wxJoin(cur, ';');
|
|
}
|
|
|
|
final.Prepend(_("All Supported Formats") + " (" + wxJoin(all, ',') + ")|" + wxJoin(all, ';'));
|
|
|
|
return final;
|
|
}
|