Aegisub/vsfilter/subtitles/USFSubtitles.cpp
Niels Martin Hansen 13654219c2 Implemented four new tags: \xbord \ybord \xshad \yshad
Allows setting the border in X and Y direction separately, and the shadow displacement in X and Y direction separately.
The \xshad and \yshad tags also allow negative shadow displacement values. Negative shadow is still not allowed in any of the traditional locations.

Originally committed to SVN as r2278.
2008-07-22 21:42:36 +00:00

773 lines
19 KiB
C++

/*
* Copyright (C) 2003-2006 Gabest
* http://www.gabest.org
*
* This Program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2, or (at your option)
* any later version.
*
* This Program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with GNU Make; see the file COPYING. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
#include "StdAfx.h"
#include "usfsubtitles.h"
#define DeclareNameAndValue(pNode, name, val) \
CComBSTR name; \
pNode->get_nodeName(&name); \
name.ToLower(); \
CComVariant val; \
pNode->get_nodeValue(&val); \
#define BeginEnumAttribs(pNode, pChild, name, value) \
{CComPtr<IXMLDOMNamedNodeMap> pAttribs; \
if(SUCCEEDED(pNode->get_attributes(&pAttribs)) && pAttribs != NULL) \
{ \
CComPtr<IXMLDOMNode> pChild; \
for(pAttribs->nextNode(&pChild); pChild; pChild = NULL, pAttribs->nextNode(&pChild)) \
{ \
#define EndEnumAttribs }}}
#define BeginEnumChildren(pNode, pChild) \
{CComPtr<IXMLDOMNode> pChild, pNext; \
for(pNode->get_firstChild(&pChild); pChild; pNext = NULL, pChild->get_nextSibling(&pNext), pChild = pNext) \
{ \
#define EndEnumChildren }}
static CStringW GetText(CComPtr<IXMLDOMNode> pNode)
{
CComBSTR bstr;
pNode->get_text(&bstr);
return(CStringW(bstr));
}
static CStringW GetXML(CComPtr<IXMLDOMNode> pNode)
{
CComBSTR bstr;
pNode->get_xml(&bstr);
CStringW str(bstr);
str.Remove('\r');
str.Replace('\n', ' ');
for(int i = 0; (i = str.Find(L" ", i)) >= 0; )
{
for(++i; i < str.GetLength() && (str[i] == '\n' || str[i] == ' ');)
str.Delete(i);
}
return(str);
}
static CStringW GetAttrib(CStringW attrib, CComPtr<IXMLDOMNode> pNode)
{
CStringW ret;
BeginEnumAttribs(pNode, pChild, name, val)
{
DeclareNameAndValue(pChild, name, val);
if(CStringW(name) == attrib && val.vt == VT_BSTR) // TODO: prepare for other types
{
ret = val.bstrVal;
break;
}
}
EndEnumAttribs
return(ret);
}
static int TimeToInt(CStringW str)
{
CAtlList<CStringW> sl;
int i = 0;
for(CStringW token = str.Tokenize(L":.,", i); !token.IsEmpty(); token = str.Tokenize(L":.,", i))
sl.AddHead(token);
if(sl.GetCount() > 4)
return(-1);
int time = 0;
int mul[4] = {1,1000,60*1000,60*60*1000};
POSITION pos = sl.GetHeadPosition();
for(i = 0; pos; i++)
{
const WCHAR* s = sl.GetNext(pos);
WCHAR* tmp = NULL;
int t = wcstol(s, &tmp, 10);
if(s >= tmp) return(-1);
time += t * mul[i];
}
return(time);
}
static DWORD StringToDWORD(CStringW str)
{
if(str.IsEmpty()) return(0);
if(str[0] == '#') return((DWORD)wcstol(str, NULL, 16));
else return((DWORD)wcstol(str, NULL, 10));
}
static DWORD ColorToDWORD(CStringW str)
{
if(str.IsEmpty()) return(0);
DWORD ret = 0;
if(str[0] == '#')
{
ret = (DWORD)wcstol(str.TrimLeft('#'), NULL, 16);
}
else
{
g_colors.Lookup(CString(str), ret);
}
ret = ((ret&0xff)<<16)|(ret&0xff00ff00)|((ret>>16)&0xff);
return(ret);
}
static int TranslateAlignment(CStringW alignment)
{
return
!alignment.CompareNoCase(L"BottomLeft") ? 1 :
!alignment.CompareNoCase(L"BottomCenter") ? 2 :
!alignment.CompareNoCase(L"BottomRight") ? 3 :
!alignment.CompareNoCase(L"MiddleLeft") ? 4 :
!alignment.CompareNoCase(L"MiddleCenter") ? 5 :
!alignment.CompareNoCase(L"MiddleRight") ? 6 :
!alignment.CompareNoCase(L"TopLeft") ? 7 :
!alignment.CompareNoCase(L"TopCenter") ? 8 :
!alignment.CompareNoCase(L"TopRight") ? 9 :
2;
}
static int TranslateMargin(CStringW margin, int wndsize)
{
int ret = 0;
if(!margin.IsEmpty())
{
ret = wcstol(margin, NULL, 10);
if(margin.Find('%') >= 0) ret = wndsize * ret / 100;
}
return(ret);
}
////////////////
CUSFSubtitles::CUSFSubtitles()
{
}
CUSFSubtitles::~CUSFSubtitles()
{
}
bool CUSFSubtitles::Read(LPCTSTR fn)
{
VARIANT_BOOL vb;
CComPtr<IXMLDOMDocument> pDoc;
if(FAILED(pDoc.CoCreateInstance(CLSID_DOMDocument))
|| FAILED(pDoc->put_async(VARIANT_FALSE))
|| FAILED(pDoc->load(CComVariant(fn), &vb)) || vb != VARIANT_TRUE)
return(false);
styles.RemoveAll();
effects.RemoveAll();
texts.RemoveAll();
if(!ParseUSFSubtitles(CComQIPtr<IXMLDOMNode>(pDoc)))
return(false);
POSITION pos = styles.GetHeadPosition();
while(pos)
{
style_t* def = styles.GetNext(pos);
if(def->name.CompareNoCase(L"Default"))
continue;
POSITION pos2 = styles.GetHeadPosition();
while(pos2)
{
style_t* s = styles.GetNext(pos2);
if(!s->name.CompareNoCase(L"Default"))
continue;
if(s->fontstyle.face.IsEmpty()) s->fontstyle.face = def->fontstyle.face;
if(s->fontstyle.size.IsEmpty()) s->fontstyle.size = def->fontstyle.size;
if(s->fontstyle.color[0].IsEmpty()) s->fontstyle.color[0] = def->fontstyle.color[0];
if(s->fontstyle.color[1].IsEmpty()) s->fontstyle.color[1] = def->fontstyle.color[1];
if(s->fontstyle.color[2].IsEmpty()) s->fontstyle.color[2] = def->fontstyle.color[2];
if(s->fontstyle.color[3].IsEmpty()) s->fontstyle.color[3] = def->fontstyle.color[3];
if(s->fontstyle.italic.IsEmpty()) s->fontstyle.italic = def->fontstyle.italic;
if(s->fontstyle.weight.IsEmpty()) s->fontstyle.weight = def->fontstyle.weight;
if(s->fontstyle.underline.IsEmpty()) s->fontstyle.underline = def->fontstyle.underline;
if(s->fontstyle.alpha.IsEmpty()) s->fontstyle.alpha = def->fontstyle.alpha;
if(s->fontstyle.outline.IsEmpty()) s->fontstyle.outline = def->fontstyle.outline;
if(s->fontstyle.shadow.IsEmpty()) s->fontstyle.shadow = def->fontstyle.shadow;
if(s->fontstyle.wrap.IsEmpty()) s->fontstyle.wrap = def->fontstyle.wrap;
if(s->pal.alignment.IsEmpty()) s->pal.alignment = def->pal.alignment;
if(s->pal.relativeto.IsEmpty()) s->pal.relativeto = def->pal.relativeto;
if(s->pal.horizontal_margin.IsEmpty()) s->pal.horizontal_margin = def->pal.horizontal_margin;
if(s->pal.vertical_margin.IsEmpty()) s->pal.vertical_margin = def->pal.vertical_margin;
if(s->pal.rotate[0].IsEmpty()) s->pal.rotate[0] = def->pal.rotate[0];
if(s->pal.rotate[1].IsEmpty()) s->pal.rotate[1] = def->pal.rotate[1];
if(s->pal.rotate[2].IsEmpty()) s->pal.rotate[2] = def->pal.rotate[2];
}
break;
}
pos = texts.GetHeadPosition();
while(pos)
{
text_t* t = texts.GetNext(pos);
if(t->style.IsEmpty()) t->style = L"Default";
}
return(true);
}
bool CUSFSubtitles::ConvertToSTS(CSimpleTextSubtitle& sts)
{
sts.m_name = metadata.language.text;
sts.m_encoding = CTextFile::UTF8;
sts.m_dstScreenSize = CSize(640, 480);
sts.m_fScaledBAS = true;
sts.m_defaultWrapStyle = 1;
// TODO: map metadata.language.code to charset num (windows doesn't have such a function...)
int charSet = DEFAULT_CHARSET;
POSITION pos = styles.GetHeadPosition();
while(pos)
{
style_t* s = styles.GetNext(pos);
if(!s->name.CompareNoCase(L"Default") && !s->fontstyle.wrap.IsEmpty())
{
sts.m_defaultWrapStyle =
!s->fontstyle.wrap.CompareNoCase(L"no") ? 2 :
!s->fontstyle.wrap.CompareNoCase(L"auto") ? 1 :
1;
}
STSStyle* stss = new STSStyle;
if(!stss) continue;
if(!s->pal.horizontal_margin.IsEmpty())
stss->marginRect.left = stss->marginRect.right = TranslateMargin(s->pal.horizontal_margin, sts.m_dstScreenSize.cx);
if(!s->pal.vertical_margin.IsEmpty())
stss->marginRect.top = stss->marginRect.bottom = TranslateMargin(s->pal.vertical_margin, sts.m_dstScreenSize.cy);
stss->scrAlignment = TranslateAlignment(s->pal.alignment);
if(!s->pal.relativeto.IsEmpty()) stss->relativeTo =
!s->pal.relativeto.CompareNoCase(L"window") ? 0 :
!s->pal.relativeto.CompareNoCase(L"video") ? 1 :
0;
stss->borderStyle = 0;
if(!s->fontstyle.outline.IsEmpty()) stss->outlineWidthX = stss->outlineWidthY = wcstol(s->fontstyle.outline, NULL, 10);
if(!s->fontstyle.shadow.IsEmpty()) stss->shadowDepthX = stss->shadowDepthY = wcstol(s->fontstyle.shadow, NULL, 10);
for(int i = 0; i < 4; i++)
{
DWORD color = ColorToDWORD(s->fontstyle.color[i]);
int alpha = (BYTE)wcstol(s->fontstyle.alpha, NULL, 10);
stss->colors[i] = color & 0xffffff;
stss->alpha[i] = (BYTE)(color>>24);
stss->alpha[i] = stss->alpha[i] + (255 - stss->alpha[i]) * min(max(alpha, 0), 100) / 100;
}
if(!s->fontstyle.face.IsEmpty()) stss->fontName = s->fontstyle.face;
if(!s->fontstyle.size.IsEmpty()) stss->fontSize = wcstol(s->fontstyle.size, NULL, 10);
if(!s->fontstyle.weight.IsEmpty()) stss->fontWeight =
!s->fontstyle.weight.CompareNoCase(L"normal") ? FW_NORMAL :
!s->fontstyle.weight.CompareNoCase(L"bold") ? FW_BOLD :
!s->fontstyle.weight.CompareNoCase(L"lighter") ? FW_LIGHT :
!s->fontstyle.weight.CompareNoCase(L"bolder") ? FW_SEMIBOLD :
wcstol(s->fontstyle.weight, NULL, 10);
if(stss->fontWeight == 0) stss->fontWeight = FW_NORMAL;
if(!s->fontstyle.italic.IsEmpty()) stss->fItalic = s->fontstyle.italic.CompareNoCase(L"yes") == 0;
if(!s->fontstyle.underline.IsEmpty()) stss->fUnderline = s->fontstyle.underline.CompareNoCase(L"yes") == 0;
if(!s->pal.rotate[0].IsEmpty()) stss->fontAngleZ = wcstol(s->pal.rotate[0], NULL, 10);
if(!s->pal.rotate[1].IsEmpty()) stss->fontAngleX = wcstol(s->pal.rotate[1], NULL, 10);
if(!s->pal.rotate[2].IsEmpty()) stss->fontAngleY = wcstol(s->pal.rotate[2], NULL, 10);
stss->charSet = charSet;
sts.AddStyle(WToT(s->name), stss);
}
pos = texts.GetHeadPosition();
while(pos)
{
text_t* t = texts.GetNext(pos);
if(!t->pal.alignment.IsEmpty())
{
CStringW s;
s.Format(L"{\\an%d}", TranslateAlignment(t->pal.alignment));
t->str = s + t->str;
}
CRect marginRect;
marginRect.SetRectEmpty();
if(!t->pal.horizontal_margin.IsEmpty())
marginRect.left = marginRect.right = TranslateMargin(t->pal.horizontal_margin, sts.m_dstScreenSize.cx);
if(!t->pal.vertical_margin.IsEmpty())
marginRect.top = marginRect.bottom = TranslateMargin(t->pal.vertical_margin, sts.m_dstScreenSize.cy);
WCHAR rtags[3][8] = {L"{\\rz%d}", L"{\\rx%d}", L"{\\ry%d}"};
for(int i = 0; i < 3; i++)
{
if(int angle = wcstol(t->pal.rotate[i], NULL, 10))
{
CStringW str;
str.Format(rtags[i], angle);
t->str = str + t->str;
}
}
if(t->style.CompareNoCase(L"Default") != 0)
{
POSITION pos = styles.GetHeadPosition();
while(pos)
{
style_t* s = styles.GetNext(pos);
if(s->name == t->style && !s->fontstyle.wrap.IsEmpty())
{
int WrapStyle =
!s->fontstyle.wrap.CompareNoCase(L"no") ? 2 :
!s->fontstyle.wrap.CompareNoCase(L"auto") ? 1 :
1;
if(WrapStyle != sts.m_defaultWrapStyle)
{
CStringW str;
str.Format(L"{\\q%d}", WrapStyle);
t->str = str + t->str;
}
break;
}
}
}
// TODO: apply effects as {\t(..)} after usf's standard clearly defines them
sts.Add(t->str, true, t->start, t->stop, WToT(t->style), _T(""), _T(""), marginRect);
}
return(true);
}
bool CUSFSubtitles::ParseUSFSubtitles(CComPtr<IXMLDOMNode> pNode)
{
DeclareNameAndValue(pNode, name, val);
if(name == L"usfsubtitles")
{
// metadata
BeginEnumChildren(pNode, pChild)
{
DeclareNameAndValue(pChild, name, val);
if(name == L"metadata")
{
ParseMetadata(pChild, metadata);
}
}
EndEnumChildren
// styles
BeginEnumChildren(pNode, pChild)
{
DeclareNameAndValue(pChild, name, val);
if(name == L"styles")
{
BeginEnumChildren(pChild, pGrandChild) // :)
{
DeclareNameAndValue(pGrandChild, name, val);
if(name == L"style")
{
CAutoPtr<style_t> s(new style_t);
if(s)
{
ParseStyle(pGrandChild, s);
styles.AddTail(s);
}
}
}
EndEnumChildren
}
}
EndEnumChildren
// effects
BeginEnumChildren(pNode, pChild)
{
DeclareNameAndValue(pChild, name, val);
if(name == L"effects")
{
BeginEnumChildren(pChild, pGrandChild) // :)
{
DeclareNameAndValue(pGrandChild, name, val);
if(name == L"effect")
{
CAutoPtr<effect_t> e(new effect_t);
if(e)
{
ParseEffect(pGrandChild, e);
effects.AddTail(e);
}
}
}
EndEnumChildren
}
}
EndEnumChildren
// subtitles
BeginEnumChildren(pNode, pChild)
{
DeclareNameAndValue(pChild, name, val);
if(name == L"subtitles")
{
BeginEnumChildren(pChild, pGrandChild) // :)
{
DeclareNameAndValue(pGrandChild, name, val);
if(name == L"subtitle")
{
CStringW sstart = GetAttrib(L"start", pGrandChild);
CStringW sstop = GetAttrib(L"stop", pGrandChild);
CStringW sduration = GetAttrib(L"duration", pGrandChild);
if(sstart.IsEmpty() || (sstop.IsEmpty() && sduration.IsEmpty()))
continue;
int start = TimeToInt(sstart);
int stop = !sstop.IsEmpty() ? TimeToInt(sstop) : (start + TimeToInt(sduration));
ParseSubtitle(pGrandChild, start, stop);
}
}
EndEnumChildren
}
}
EndEnumChildren
return(true);
}
BeginEnumChildren(pNode, pChild)
{
if(ParseUSFSubtitles(pChild))
{
return(true);
}
}
EndEnumChildren
return(false);
}
void CUSFSubtitles::ParseMetadata(CComPtr<IXMLDOMNode> pNode, metadata_t& m)
{
DeclareNameAndValue(pNode, name, val);
if(name == L"title")
{
m.title = GetText(pNode);
}
else if(name == L"date")
{
m.date = GetText(pNode);
}
else if(name == L"comment")
{
m.comment = GetText(pNode);
}
else if(name == L"author")
{
BeginEnumChildren(pNode, pChild)
{
DeclareNameAndValue(pChild, name, val);
if(name == L"name")
m.author.name = GetText(pChild);
else if(name == L"email")
m.author.email = GetText(pChild);
else if(name == L"url")
m.author.url = GetText(pChild);
}
EndEnumChildren
return;
}
else if(name == L"language")
{
m.language.text = GetText(pNode);
m.language.code = GetAttrib(L"code", pNode);
}
else if(name == L"languageext")
{
m.languageext.text = GetText(pNode);
m.languageext.code = GetAttrib(L"code", pNode);
}
BeginEnumChildren(pNode, pChild)
{
ParseMetadata(pChild, metadata);
}
EndEnumChildren
}
void CUSFSubtitles::ParseStyle(CComPtr<IXMLDOMNode> pNode, style_t* s)
{
DeclareNameAndValue(pNode, name, val);
if(name == L"style")
{
s->name = GetAttrib(L"name", pNode);
}
else if(name == L"fontstyle")
{
ParseFontstyle(pNode, s->fontstyle);
return;
}
else if(name == L"position")
{
ParsePal(pNode, s->pal);
return;
}
BeginEnumChildren(pNode, pChild)
{
ParseStyle(pChild, s);
}
EndEnumChildren
}
void CUSFSubtitles::ParseFontstyle(CComPtr<IXMLDOMNode> pNode, fontstyle_t& fs)
{
fs.face = GetAttrib(L"face", pNode);
fs.size = GetAttrib(L"size", pNode);
fs.color[0] = GetAttrib(L"color", pNode);
fs.color[1] = GetAttrib(L"back-color", pNode);
fs.color[2] = GetAttrib(L"outline-color", pNode);
fs.color[3] = GetAttrib(L"shadow-color", pNode);
fs.italic = GetAttrib(L"italic", pNode);
fs.weight = GetAttrib(L"weight", pNode);
fs.underline = GetAttrib(L"underline", pNode);
fs.alpha = GetAttrib(L"alpha", pNode);
fs.outline = GetAttrib(L"outline-level", pNode);
fs.shadow = GetAttrib(L"shadow-level", pNode);
fs.wrap = GetAttrib(L"wrap", pNode);
}
void CUSFSubtitles::ParsePal(CComPtr<IXMLDOMNode> pNode, posattriblist_t& pal)
{
pal.alignment = GetAttrib(L"alignment", pNode);
pal.relativeto = GetAttrib(L"relative-to", pNode);
pal.horizontal_margin = GetAttrib(L"horizontal-margin", pNode);
pal.vertical_margin = GetAttrib(L"vertical-margin", pNode);
pal.rotate[0] = GetAttrib(L"rotate-z", pNode);
pal.rotate[1] = GetAttrib(L"rotate-x", pNode);
pal.rotate[2] = GetAttrib(L"rotate-y", pNode);
}
void CUSFSubtitles::ParseEffect(CComPtr<IXMLDOMNode> pNode, effect_t* e)
{
DeclareNameAndValue(pNode, name, val);
if(name == L"effect")
{
e->name = GetAttrib(L"name", pNode);
}
else if(name == L"keyframes")
{
BeginEnumChildren(pNode, pChild)
{
DeclareNameAndValue(pChild, name, val);
if(name == L"keyframe")
{
CAutoPtr<keyframe_t> k(new keyframe_t);
if(k)
{
ParseKeyframe(pChild, k);
e->keyframes.AddTail(k);
}
}
}
EndEnumChildren
return;
}
BeginEnumChildren(pNode, pChild)
{
ParseEffect(pChild, e);
}
EndEnumChildren
}
void CUSFSubtitles::ParseKeyframe(CComPtr<IXMLDOMNode> pNode, keyframe_t* k)
{
DeclareNameAndValue(pNode, name, val);
if(name == L"keyframe")
{
k->position = GetAttrib(L"position", pNode);
}
else if(name == L"fontstyle")
{
ParseFontstyle(pNode, k->fontstyle);
return;
}
else if(name == L"position")
{
ParsePal(pNode, k->pal);
return;
}
}
void CUSFSubtitles::ParseSubtitle(CComPtr<IXMLDOMNode> pNode, int start, int stop)
{
DeclareNameAndValue(pNode, name, val);
if(name == L"text" || name == L"karaoke")
{
CAutoPtr<text_t> t(new text_t);
if(t)
{
t->start = start;
t->stop = stop;
t->style = GetAttrib(L"style", pNode);
t->effect = GetAttrib(L"effect", pNode);
ParsePal(pNode, t->pal);
ParseText(pNode, t->str);
texts.AddTail(t);
}
return;
}
// else if
BeginEnumChildren(pNode, pChild)
{
ParseSubtitle(pChild, start, stop);
}
EndEnumChildren
}
void CUSFSubtitles::ParseText(CComPtr<IXMLDOMNode> pNode, CStringW& str)
{
DeclareNameAndValue(pNode, name, val);
CStringW prefix, postfix;
if(name == L"b")
{
prefix = L"{\\b1}";
postfix = L"{\\b}";
}
else if(name == L"i")
{
prefix = L"{\\i1}";
postfix = L"{\\i}";
}
else if(name == L"u")
{
prefix = L"{\\u1}";
postfix = L"{\\u}";
}
else if(name == L"font")
{
fontstyle_t fs;
ParseFontstyle(pNode, fs);
if(!fs.face.IsEmpty()) {prefix += L"{\\fn" + fs.face + L"}"; postfix += L"{\\fn}";}
if(!fs.size.IsEmpty()) {prefix += L"{\\fs" + fs.size + L"}"; postfix += L"{\\fs}";}
if(!fs.outline.IsEmpty()) {prefix += L"{\\bord" + fs.outline + L"}"; postfix += L"{\\bord}";}
if(!fs.shadow.IsEmpty()) {prefix += L"{\\shad" + fs.shadow + L"}"; postfix += L"{\\shad}";}
for(int i = 0; i < 4; i++)
{
if(!fs.color[i].IsEmpty())
{
CStringW s;
s.Format(L"{\\%dc&H%06x&}", i+1, ColorToDWORD(fs.color[i]));
prefix += s;
s.Format(L"{\\%dc}", i+1);
postfix += s;
}
}
}
else if(name == L"k")
{
int t = wcstol(GetAttrib(L"t", pNode), NULL, 10);
CStringW s;
s.Format(L"{\\kf%d}", t / 10);
str += s;
return;
}
else if(name == L"br")
{
str += L"\\N";
return;
}
else if(name == L"#text")
{
str += GetXML(pNode);
return;
}
BeginEnumChildren(pNode, pChild)
{
CStringW s;
ParseText(pChild, s);
str += s;
}
EndEnumChildren
str = prefix + str + postfix;
}
void CUSFSubtitles::ParseShape(CComPtr<IXMLDOMNode> pNode)
{
// no specs on this yet
}