#!/data/data/com.termux/files/usr/bin/env python3
# -*- coding: UTF-8 -*-

#    srt2vobsub - a simple command-line vobsub subtitles generator
#    Copyright (C) 2020  Michael Lange <klappnase@users.sf.net>
#
#    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 3 of the License, 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 this program.  If not, see <http://www.gnu.org/licenses/>.

__VERSION__ = '1.0'

import argparse
from ast import literal_eval
import chardet
import codecs
import datetime
import os
import srt
from srt import ZERO_TIMEDELTA
import re
import shutil
from subprocess import call, Popen, PIPE
import sys
import tempfile
import textwrap
import wand
from wand.image import Image
from wand.color import Color
from wand.drawing import Drawing

import time
stime = time.time()

###########################################################################

############    config section   ####################################

DEFAULTS = {
'REMOVE_HI': '0',
'VERBOSE': 1,
'LANGUAGE': 'ja',
'ENCODING': 'auto',
'ENCODINGERRORS': 'strict',
'SAVESRT': '0',
'FONT': 'DroidSans',
'FONTSIZE': 5.0,
'MAXLINESIZE': 60,
'OUTLINEWIDTH': 1,
'BOXPADDING': 'auto',
'YOFFSET': 5,
'ANTIALIASING': '0',
'FILLCOLOR': '#ffffff',
'OUTLINECOLOR': '#000000',
'BOXCOLOR': 'none',
'RESOLUTION': '1080p',
'FRAMERATE': 24,
'MINDISPLAYTIME': 500,
'MAXDISPLAYTIME': 8000,
'DEFAULTDISPLAYTIME': 2500,
'FIXTIMES': 1,
'BDSUP2SUBVERSION':'auto',
'BDSUP2SUBRUNTIME': '/data/data/com.termux/files/usr/share/java/bdsup2sub.jar',
'BDSUP2SUBOPTS': '()'
}

    #####   a few helpers for custom option types   ###########

def boxpadding(string):
    # see if the value given to --boxpadding is valid
    # (two integers separated by a comma, each one greater than -1)
    if string.lower() == 'auto':
        x, y = -1, -1
    else:
        try:
            x, y = string.split(',')
            x = int(x.strip())
            y = int(y.strip())
        except:
            raise TypeError
        if x < -1 or y < -1:
            raise TypeError
    return (x, y)

def options_tuple(string):
    # the options tuple passed to --bdsup2suboptions must consist entirely
    # of string values;
    # if the users accidentally passed a tuple containing e.g. an integer
    # instead of a string, see if we can fix this
    tup = literal_eval(string)
    if not isinstance(tup, tuple):
        raise TypeError
    res = ()
    for val in tup:
        val = str(val)
        res += (val,)
    return res

def bdsup2subversion(string):
    s = string.lower()
    if s in ('auto', '4', '5'):
        return s
    else:
        raise TypeError

def getboolean(string):
    # boolean values like true/false etc. should also be accepted
    # for boolean options
    if string.lower() in ('1', 'yes', 'true', 'on'):
        return True
    elif string.lower() in ('0', 'no', 'false', 'off'):
        return False
    else:
        raise TypeError

def getencoding(string):
    if string.lower() == 'auto':
        return 'auto'
    # check if the given encoding exists
    try:
        codecs.lookup(string)
        return string
    except:
        raise TypeError

def fontsize(string):
    # must be either an int > 0 as point size or a float value as percent
    # of screen height
    try:
        size = int(string)
        if not size > 0:
            raise TypeError
    except:
        try:
            size = float(string)
            if not 1.0 <= size <= 10.0:
                raise TypeError
        except:
            raise TypeError
    return size

# helper class for argparse; if we pass e.g. range(0,256) to choices, the help
# text would display the whole list of ints
class IntRange(list):
    def __init__(self, low, high):
        self.values = range(low, high)
        self.append('%d-%d' % (low, high-1))
    def __contains__(self, value):
        if value in self.values:
            return True
        return False

# choices list for the --resolution option that handles values case insensitive
class ResolutionList(list):
    def __init__(self):
        self.values = ('1080p', '720p', '1440x1080', 'pal', 'ntsc')
        for v in self.values:
            self.append(v)
    def __contains__(self, value):
        if value.lower() in self.values:
            return True
        return False
resolutionlist = ResolutionList()

    #########   main functions for configuration handling   ######

def getconfig(config):
    '''Read default values from the configuration file.'''
    try:
        f = open(os.path.join(os.getenv('HOME'),
                            '.config', 'srt2vobsub', 'defaults.conf'), 'r')
        cont = f.read().splitlines()
        f.close()
    except:
        return config
    int_opts = ('MAXLINESIZE',
                'OUTLINEWIDTH', 'YOFFSET', 'FRAMERATE',
                'VERBOSE', 'MINDISPLAYTIME',
                'MAXDISPLAYTIME', 'DEFAULTDISPLAYTIME')
    for line in cont:
        line = line.strip()
        if not line or line.startswith('#'):
            continue
        try:
            key, value = line.split('=')
        except:
            continue
        # on the command line the user has to enclose the bdsup2subopts tuple
        # in quotes, so they'll probably try to do the same in the config
        # file; so check if there are quotes and if yes, silently
        # remove them
        key, value = key.strip(), value.strip().strip('"')
        if key in ('ANTIALIASING', 'FIXTIMES', 'SAVESRT'):
                if value.lower() in ('1', 'yes', 'true', 'on', '0', 'no', 'false', 'off'):
                    config[key] = getboolean(value)
        elif key in int_opts:
            try:
                value = int(value)
            except:
                continue
            if key == 'VERBOSE' and value in (0, 1, 2):
                config[key] = value
            elif key == 'BDSUP2SUBVERSION' and value.lower() in ('auto', '4', '5'):
                config[key] = value
            elif key == 'FRAMERATE' and value in (24, 25, 30):
                config[key] = value
            elif key == 'MINDISPLAYTIME' and value in range(0, 2001):
                config[key] = value
            elif key == 'MAXDISPLAYTIME' and value in range(500, 20001):
                config[key] = value
            elif key == 'DEFAULTDISPLAYTIME' and value in range(500, 5001):
                config[key] = value
            elif key == 'MAXLINESIZE' and value in range(35, 101):
                config[key] = value
            elif key == 'OUTLINEWIDTH' and value in range(0, 11):
                config[key] = value
            elif key == 'YOFFSET' and value in range(0, 101):
                config[key] = value
        else:
            if key == 'RESOLUTION':
                if value in resolutionlist:
                    config[key] = value.lower()
            if key == 'FONTSIZE':
                try:
                    config[key] = fontsize(value)
                except:
                    pass
            elif key == 'REMOVE_HI':
                if value.lower() in ('none', 'b', 'p', 'bp'):
                    config[key] = value.lower()
            elif key == 'BDSUP2SUBOPTS':
                try:
                    res = options_tuple(value)
                    config[key] = value
                except:
                    continue
            elif key == 'BOXPADDING':
                try:
                    config[key] = boxpadding(value)
                except:
                    continue
            elif key == 'ENCODING':
                try:
                    config[key] = getencoding(value)
                except:
                    continue
            elif key == 'ENCODINGERRORS':
                if value.lower() in ('strict', 'replace'):
                    config[key] = value.lower()
            else:
                config[key] = value
    return config

license_file = '/data/data/com.termux/files/usr/share/doc/srt2vobsub/LICENSE'
license_info = '''srt2vobsub  Copyright (C) 2020  Michael Lange
    This program comes with ABSOLUTELY NO WARRANTY; for details type
    `cat %s'.
    This is free software, and you are welcome to redistribute it under certain conditions;
    type `cat %s' for details.
''' % (license_file, license_file)

def parse_arguments():
    '''Parse commandline arguments.'''
    parser = argparse.ArgumentParser(prog='srt2vobsub',
            formatter_class=argparse.RawDescriptionHelpFormatter,
            description=license_info + '\n' +\
                        'srt2vobsub converts a text subtitles file to ' +\
                        'a pair of .sub/.idx vobsub subtitle files.')
    parser.add_argument('-v', '--verbose', dest='VERBOSE',
            help='print informative messages to stdout', type=int,
            default=CONFIG['VERBOSE'], choices=range(3))
    parser.add_argument('-V', '--version', action='version',
            version='srt2vobsub %s' % __VERSION__)
    parser.add_argument('-n', '--name', dest='NAME',
            help='basename of the .sub/.idx output files ' +\
                 '(default: basename of input file)',
            default='')
    parser.add_argument('-d', '--directory', dest='DIRECTORY',
            help='target directory of the output files ' +\
                 '(default: directory of input file)',
            default='')
    parser.add_argument('-i', '--remove-hi', dest='REMOVE_HI',
            help='remove subtitles for the hearing impaired (default: off)',
            default=CONFIG['REMOVE_HI'], choices=('none', 'b', 'p', 'bp'))

    parser.add_argument('-l', '--lang', dest='LANGUAGE',
            help='subtitles language descriptor (default: "la")',
            default=CONFIG['LANGUAGE'])
    parser.add_argument('-e', '--encoding', dest='ENCODING',
            help='text encoding to use (default: "auto")',
            type=getencoding, default=CONFIG['ENCODING'])
    parser.add_argument('-E', '--encodingerrors', dest='ENCODINGERRORS',
            help='how to treat encoding errors; default: strict',
            default=CONFIG['ENCODINGERRORS'], choices=('strict', 'replace'))
    parser.add_argument('-S', '--savesrt', dest='SAVESRT',
            help='save processed srt file; default: 0',
            type=getboolean, default=CONFIG['SAVESRT'])
    parser.add_argument('-M', '--mindisplaytime', dest='MINDISPLAYTIME',
            help='min. time to display subtitles in ms; default: 500',
            type=int, default=CONFIG['MINDISPLAYTIME'], choices=IntRange(0, 2001))
    parser.add_argument('-X', '--maxdisplaytime', dest='MAXDISPLAYTIME',
            help='max. time to display subtitles in ms; default: 8000',
            type=int, default=CONFIG['MAXDISPLAYTIME'], choices=IntRange(3000, 20001))
    parser.add_argument('-D', '--defaultdisplaytime', dest='DEFAULTDISPLAYTIME',
            help='default time to display subtitles in ms; default: 2500',
            type=int, default=CONFIG['DEFAULTDISPLAYTIME'], choices=IntRange(500, 5001))
    parser.add_argument('-F', '--fixtimes', dest='FIXTIMES',
            help='fix time-code errors in srt file (default: on)',
            type=getboolean, default=CONFIG['FIXTIMES'])

    parser.add_argument('-f', '--framerate', dest='FRAMERATE',
            help='video framerate (default: 24)', type=int,
            default=CONFIG['FRAMERATE'], choices=(24, 25, 30))
    parser.add_argument('-r', '--resolution', dest='RESOLUTION',
            help='video resolution (default: 1080p)', default=CONFIG['RESOLUTION'],
            choices=resolutionlist)
    parser.add_argument('-m', '--movie', dest='MOVIE',
            help='detect resolution and framerate from movie file', default='')

    parser.add_argument('-t', '--font', dest='FONT',
            help='font to use; default: DroidSans',
            default=CONFIG['FONT'])
    parser.add_argument('-g', '--antialiasing', dest='ANTIALIASING',
            help='enable font antialiasing (default: off)', type=getboolean,
            default=CONFIG['ANTIALIASING'])
    parser.add_argument('-s', '--fontsize', dest='FONTSIZE',
            help='fontsize to use (default: 0=auto)', type=fontsize,
            default=CONFIG['FONTSIZE'])
    parser.add_argument('-z', '--maxlinesize', dest='MAXLINESIZE',
            help='max. size of a text line (in percent of screen width); default: 60',
            type=int, default=CONFIG['MAXLINESIZE'], choices=IntRange(35, 101))
    parser.add_argument('-w', '--outlinewidth', dest='OUTLINEWIDTH',
            help='width of the font outline; default: 1', type=int,
            default=CONFIG['OUTLINEWIDTH'], choices=IntRange(0,11))
    parser.add_argument('-y', '--yoffset', dest='YOFFSET',
            help='distance between bottom of the screen and subtitles text',
            type=int, default=CONFIG['YOFFSET'], choices=IntRange(0, 101))
    parser.add_argument('-B', '--boxpadding', dest='BOXPADDING',
            help="padding of the text's bounding box",
            type=boxpadding, default=CONFIG['BOXPADDING'])

    parser.add_argument('-c', '--fillcolor', dest='FILLCOLOR',
            help='text fill color; default: white', default=CONFIG['FILLCOLOR'])
    parser.add_argument('-o', '--outlinecolor', dest='OUTLINECOLOR',
            help='text outline color; default: black',
            default=CONFIG['OUTLINECOLOR'])
    parser.add_argument('-b', '--boxcolor', dest='BOXCOLOR',
            help='color of the box surrounding the text; default: none',
            default=CONFIG['BOXCOLOR'])

    parser.add_argument('-j', '--bdsup2subruntime', dest='BDSUP2SUBRUNTIME',
            help='path to the bdsup2sub runtime',
            default=CONFIG['BDSUP2SUBRUNTIME'])
    parser.add_argument('-p', '--bdsup2subopts', dest='BDSUP2SUBOPTS',
            help='options passed to bdsup2sub', type=options_tuple,
            default=CONFIG['BDSUP2SUBOPTS'])
    parser.add_argument('-P', '--bdsup2subversion', dest='BDSUP2SUBVERSION',
            help='bdsup2sub version in use (4, 5 or auto); default: auto',
            default=CONFIG['BDSUP2SUBVERSION'], type=bdsup2subversion)

    parser.add_argument('-k', '--keeptemp', dest='KEEPTEMP',
            help='keep temporary files for debugging (default: off)',
            action='store_true')

    parser.add_argument('SUBTITLES',
                        help='text subtitles file to process')

    options = parser.parse_args()
    return options


####################   misc helper functions   ##############################

def done(sts):
    '''Clean up at program exit.'''
    os.chdir(CWD)
    del_tempdir = True
    if TEMPDIR:
        if OPTS.KEEPTEMP:
            if os.path.isdir(TEMPDIR) and os.listdir(TEMPDIR):
                # preserve the tempdir only if there is actually already
                # some content, otherwise discard it silently
                del_tempdir = False
        if del_tempdir:
            try:
                shutil.rmtree(TEMPDIR)
                printmsg('Removed temporary directory %s' % TEMPDIR, verbosity=2)
                if sts == 0:
                    printmsg('Process successfully finished.')
            except:
                printerror('Error while trying to remove directory "%s".' % TEMPDIR)
        else:
            printerror('Not deleting temporary files in "%s" due to user request.' % TEMPDIR)
    sys.exit(sts)

def detect_encoding(filename):
    try:
        f = open(filename, 'rb')
        cont = f.read()
        f.close()
        det = chardet.detect(cont)
        return det['encoding'], det['confidence']
    except:
        return None, None

def getbdsup2subversion(string):
    if string in ('4', '5'):
        printmsg('Info: using bdsup2sub version %s' % string)
        return string
    elif string.lower() == 'auto':
        # this is tricky, because there is no option to query the version
        # in bdsup2sub v.4; as a workaround we use the options '-V foo' here
        # which will return the version with v.5 just as if the "foo" was
        # not there, but will be considered an illegal command with v.4
        # and cause bdsup2sub to stop with error 1 (beware: calling
        # bdsup2sub v.4 with just the -V option would open a gui window!)
        cmd = ('java', '-jar', OPTS.BDSUP2SUBRUNTIME, '-V', 'foo')
        sts, out, err = getstatusoutput(cmd)
        if sts == 0:
            v = '5'
        else:
            v = '4'
        printmsg('Info: detected bdsup2sub version %s' % v)
        return v

def getfont(fontdesc):
    '''If the requested font is not found, see if we can fall back to
    DejaVuSans. Return the full path to the font file or None if no
    matching font was found.'''
    if os.path.isfile(fontdesc):
        return fontdesc
    font = None
    fclist = which('fc-list')
    if fclist:
        sts, out, err = getstatusoutput((fclist, ':', 'file'))
        if sts:
            printerror('Error while trying to detect font "%s".')
        else:
            # an output line looks like:
            # /usr/share/fonts/truetype/msttcorefonts/Arial.ttf:
            lines = [x.strip().strip(':') for x in out.splitlines()]
            for line in lines:
                base, ext = os.path.splitext(os.path.basename(line))
                if base == fontdesc:
                    font = line
                    printmsg('Info: using font file: %s' % font, verbosity=2)
                    break
            if not font:
                # try to find system DejaVuSans first
                printerror('Error: requested font not found: %s.' % fontdesc)
                for line in lines:
                    base, ext = os.path.splitext(os.path.basename(line))
                    if base == 'DroidSans':
                        font = line
                        printerror('Will use %s instead.' % line)
                        break
    else:
        printerror('Error: unable to detect font "%s", command "fc-list" not found.')
    return font

def getstatusoutput(args, shell=False):
    pipe = Popen(args, stdout=PIPE, stderr=PIPE, bufsize=-1, shell=shell)
    output, errors = pipe.communicate()
    status = pipe.returncode
    del(pipe)
    return (status, output.decode('utf-8', errors='replace'),
            errors.decode('utf-8', errors='replace'))

def printerror(msg):
    if not msg.endswith('\n'):
        msg += '\n'
    sys.stderr.write(msg)
    sys.stderr.flush()

def printmsg(*msg, verbosity=1):
    if OPTS.VERBOSE >= verbosity:
        print(*msg)

def save_file(tmpfile, target):
    dirname, basename = os.path.dirname(target), os.path.basename(target)
    base, ext = os.path.splitext(target)
    if os.path.exists(target):
        bkup = os.path.join(dirname, '%s_bak%s' % (base, ext))
        if os.path.exists(bkup):
            try:
                os.remove(bkup)
                printerror('Warning: removed file "%s".' % bkup)
            except:
                printerror('Error: unable to remove file "%s".' % bkup)
        try:
            os.rename(target, bkup)
            printmsg('%s -> %s' % (target, bkup), verbosity=2)
        except:
            printerror('Error: unable to create backup file "%s".' % bkup)
    try:
        shutil.copy(tmpfile, target)
        printmsg('%s -> %s' % (tmpfile, target))
        return 0
    except:
        printerror('Error: unable to save file "%s".' % bkup)
        return 1

def timedelta2xmltime(timedelta):
    '''Convert the srt module's timedelta obeject into an xml timestamp.'''
    srttime = srt.timedelta_to_srt_timestamp(timedelta)
    head, msec = srttime.split(',')
    frames = str(int(round(int(msec) / 1000 * FRAMERATE))).zfill(2)
    return '%s:%s' % (head, frames)

def which(*alternatives, **kw):
    paths = os.getenv('PATH').split(os.pathsep)
    for a in alternatives:
        for p in paths:
            full = os.path.join(p, a)
            if os.path.isfile(full) and os.access(full, os.X_OK):
                return full
    if len(alternatives) > 1:
        a = '" or "'.join(alternatives)
    printerror('No "%s" in %s.' % (a, os.getenv('PATH')))
    return ''

##############################################################################
##############################################################################

#########   helper funcs for image rendering   #################

def swrap(string):
    # "smart" text wrapping function;
    # by default, textwrap.wrap() tends to break a long line into another
    # long line and a single word if the given max. length is high (like
    # 0.9 * len(string)) or into three lines if something like 0.6 * len(string)
    # is used, especially if the middle of the string contains a rather long
    # word; this function tries its best to generate always only two lines
    # that are at least approximately equally long
    # passing 0 to wrap() causes an error, might happen with weird command
    # line opts
    if len(string) == 1:
        # now we're in trouble; this may happen if the user uses an insanely
        # high value for x box padding; this would cause an infinite loop
        # since we are never able to wrap the string so that it fits into
        # one line, we have to get out of this at some other point
        return [string]
    l = (int(len(string) / 2))
    while True:
        res = textwrap.wrap(string, l, break_long_words=False)
        l += 1
        if len(res) == 2:
            break
        if l == len(string):
            # ok, giving up
            res = textwrap.wrap(string, int(l * 0.9))
            break
    return res

def wrap_lines(lines, maxwidth, image, draw):
    # returns a new list of lines, the max. line width, a list of all
    # line widths and True if all lines fit in maxwidth without wrapping,
    # else False;
    # this must be repeated until good == True
    wmax = 0
    good = True
    res = []
    widths = []
    for line in lines:
        m = draw.get_font_metrics(image, line)
        w, h = int(m.text_width), int(m.text_height)
        widths.append(w)
        if w > wmax:
            wmax = w
        if w > maxwidth:
            # split this string in two and start all over again
            wrapped = swrap(line)
            if wrapped == [line]:
                # no wrapping possible, probably due to some insane options
                # passed by the user; try to get out of this gracefully
                res += [line]
            else:
                res += wrapped
                good = False
        else:
            res += [line]
    return res, wmax, widths, good

def fix_caption(lines, maxwidth, image, draw):
    # lines is the list of lines from one caption,
    # maxwidth the max. allowed width for a caption line
    # draw and image our Image and Draw objects
    # now feed our caption lines to the line-wrapper until nothing needs
    # to be wrapped; return the new list of lines, the max. line width
    # and a list of all line widths
    good = False
    while not good:
        lines, wmax, widths, good = wrap_lines(lines, maxwidth, image, draw)
    return lines, wmax, widths

##########   main func for image rendering   ##############

def export_caption(caption, filename):
    '''Creates a .png file <filename> suitable for bdsup2sub that displays
    the given caption.'''
    img = Image(width=XSIZE, height=YSIZE)
    # by default wand will use the text's baseline as y-coord which is bad,
    # because we'd never know where we end, so set the gravity to north_west
    # Beware: it seems like if we set text_alignment='center' so we could
    # use wand's multiline text feature the gravity setting will be lost
    # irrevocably, so we need to deal with multiline captions ourselves
    img.gravity = 'north_west'
    box = Drawing()
    # Color() handles 'none' by itself, case-independently
    box.fill_color = Color(OPTS.BOXCOLOR)

    draw = Drawing()
    draw.fill_color = Color(OPTS.FILLCOLOR)
    draw.font = FONT
    draw.font_size = FONTSIZE
    draw.text_encoding = ENCODING
    if OPTS.ANTIALIASING:
        draw.text_antialias = True
    else:
        draw.text_antialias = False
    if OUTLINEWIDTH:
        draw.stroke_color = Color(OPTS.OUTLINECOLOR)
        draw.stroke_width = OUTLINEWIDTH
        if OPTS.ANTIALIASING:
            draw.stroke_antialias = True
        else:
            draw.stroke_antialias = False

    lines = [line.strip() for line in caption.splitlines() if line.strip()]

    # padding for the text's bounding box
    padx, pady = BOXPADDING

    # calculate the max. allowed text width, and don't forget the outline but
    # ignore the box; keep a few extra px. (1% of XSIZE) in reserve,
    # just to be on the safe side in case we have rounding errors
    max_textwidth = int(round(XSIZE * MAXLINESIZE / 100)) - \
                    2 * OUTLINEWIDTH - int(round(0.01 * XSIZE))

    # now loop through the list of strings, calculate each string's width and
    # if necessary split it in two;
    # repeat this until no line needs to be wrapped
    lines, wmax, line_widths = fix_caption(lines, max_textwidth, img, draw)

    # collect info for which lines we need y box padding;
    # this seems useful to avoid a too big offset between text lines,
    # which may look odd, esp. when the box is transparent
    i = 0
    padinfo = []
    for lw in line_widths:
        a, b, c = 0, 0, 0 # top padding, bottom padding, Y incr. of next line
        if i == 0:
            # the first line needs top padding
            a = pady
        elif line_widths[i-1] < lw:
            # previous line is smaller, so we need top padding
            a = pady
        if i == len(line_widths) - 1:
            # the last line needs bottom padding
            b = pady
        elif line_widths[i+1] < lw:
            # next line is smaller, so we need bottom padding
            b = pady
        elif line_widths[i+1] > lw:
            # next line is bigger, shift the Y-increment by pady
            c = pady
        i += 1
        padinfo.append((a, b, c))

    # width of the box == max. textwidth + 2* (boxpad + outline width)
    wmax = wmax + 2*padx + 2*OUTLINEWIDTH

    # now all lines should be safe to fit on the screen,
    # so start drawing
    Y = 0
    i = 0
    bottom = 0
    for line in lines:
        s = line.strip()
        m = draw.get_font_metrics(img, line)
        w, h = int(m.text_width), int(m.text_height)

        # make sure the smaller line appears centered relative to the
        # bigger line
        if wmax > XSIZE:
            # may happen if an insane x box padding was given;
            # make sure the text will be centered anyway, the excess box
            # will be chopped off later
            X = int((XSIZE - w) / 2)
        else:
            X = int((wmax - w) / 2)

        a, b, c = padinfo[i]
        # draw the bounding box
        if OPTS.BOXCOLOR.lower() != 'none':
            box.rectangle(X - OUTLINEWIDTH - padx,
                          Y - OUTLINEWIDTH - a,
                          X + w + OUTLINEWIDTH + padx,
                          Y + h + OUTLINEWIDTH + b)
            box(img)

        # draw the text and shift the Y position to the correct
        # coordinate for the following line (if any)
        draw.text(X, Y, s)
        draw(img)

        # store bottom Y coord
        bottom = Y + h + OUTLINEWIDTH + b
        Y = bottom + c + OUTLINEWIDTH + 1 # 1 px. extra so the boxes won't overlap
        i += 1

    x0, y0, x1, y1 = 0, 0, wmax + 1, bottom + 1
    # cut the text box from the surrounding void
    if y1 > YSIZE:
        # may happen if the font is too big or an insane y boxpadding is set
        printerror('\nError: generated subtitle image exceeds screen height')
        printerror('The offending subtitle text was:')
        for line in caption.splitlines():
            printerror('    ' + line)
        printerror('This subtitle will not look as expected.')
        y1 = YSIZE

    if x1 > XSIZE:
        # may happen if the user set insane boxpadding values, making
        # it impossible to wrap the text to fit into one line
        printerror('\nError: generated subtitle image exceeds screen width')
        printerror('The offending subtitle text was:')
        for line in caption.splitlines():
            printerror('    ' + line)
        printerror('This subtitle may not look as expected.')
        x1 = XSIZE

    img.crop(x0, y0, x1, y1)

    # write file and return the size for our xml content
    img.format = 'png'
    img.save(filename=filename)
    box.destroy()
    draw.destroy()
    img.destroy()
    return wmax+1, bottom+1

###########################################################################
###           MAIN PROGRAM CODE                                         ###
###########################################################################

#############  process command line options  #################################

TEMPDIR = None
CWD = os.getcwd()

CONFIG = getconfig(DEFAULTS)
OPTS = parse_arguments()
INFILE = os.path.realpath(OPTS.SUBTITLES)
if not os.path.isfile(INFILE):
    printerror('File not found: "%s", exit.' % INFILE)
    done(1)
WD = os.path.dirname(INFILE)
NAME = OPTS.NAME or os.path.splitext(os.path.basename(INFILE))[0]
DIRECTORY = OPTS.DIRECTORY
if DIRECTORY:
    if os.path.isdir(DIRECTORY):
        DIRECTORY = os.path.realpath(DIRECTORY)
        if not os.access(DIRECTORY, os.W_OK):
            printerror('Missing write permission for target directory: "%s", exit.' % DIRECTORY)
            done(1)
    else:
        printerror('Target directory not found: "%s", exit.' % DIRECTORY)
        done(1)
else:
    DIRECTORY = WD

if not os.path.isfile(OPTS.BDSUP2SUBRUNTIME):
    printerror('bdsup2sub runtime not found at "%s", exit.' % OPTS.BDSUP2SUBRUNTIME)
    sys.exit(1)

BDSUP2SUBVERSION = getbdsup2subversion(OPTS.BDSUP2SUBVERSION)

#####################  video properties   ##########

# presets as supported by bdsup2sub
RES_PRESETS = {'1080p': (1920, 1080), '1440x1080': (1440, 1080),
               '720p': (1280, 720), 'pal': (720, 576), 'ntsc': (720, 480)}
RESOLUTION = OPTS.RESOLUTION.lower()
FRAMERATE = OPTS.FRAMERATE

# see if auto-detection of video size and framerate is requested
# this will override the manually given video size and frame rate
if OPTS.MOVIE:
    MOVIE = os.path.abspath(OPTS.MOVIE)
else:
    MOVIE = None

if MOVIE:
    # do we have mediainfo?
    printmsg('Auto-detection of video size and frame rate requested.', verbosity=2)
    printmsg('This will override --framerate and --resolution if given.', verbosity=2)
    if not os.path.isfile(MOVIE):
        printerror('File not found: "%s", exit.' % MOVIE)
        sys.exit(1)
    minfo = which('mediainfo')
    if not minfo:
        printerror('Could not find mediainfo on your system, exit.')
        sys.exit(1)
    # beware: we have to omit the double-quotes the man page suggests
    cmd = [minfo, '--Inform=Video;%Width% %Height% %FrameRate%', MOVIE]
    sts, out, err = getstatusoutput(cmd)
    if sts:
        if err:
            printerror(err)
        printerror('Error while trying to query video properties, exit.')
        sys.exit(1)
    # the output should look like:
    # 1440 1080 25.000
    try:
        w, h, fps = out.strip().split()
        vidwidth = int(w)
        vidheight = int(h)
        frate = int(round(float(fps)))
        printmsg('Detected video frame rate %s fps' % fps)
        if not frate in (24, 25, 30):
            printerror('Warning: detected unsupported frame rate %s, using 24 instead' % fps)
            frate = 24
        printmsg('Detected video size: %dx%d' %(vidwidth, vidheight))
        FRAMERATE = frate

        # see if video size matches one of our presets
        # here I never got anywhere with "pal" or "ntsc", so skip these
        res = None
        for key in ('1080p', '720p', '1440x1080'):
            if RES_PRESETS[key] == (vidwidth, vidheight):
                res = key
                break
        if res:
            # video format is supported
            RESOLUTION = res
        else:
            # try to find the best supported format depending on the
            # video's aspect ratio and size
            par = round(vidwidth / vidheight, 2)
            if par == 1.33:
                res = '1440x1080'
                printerror('Unsupported video format detected: %dx%d, using "%s".' %(vidwidth, vidheight, res))
            elif par == 1.78:
                if vidwidth > 1280:
                    res = '1080p'
                else:
                    res = '720p'
                printerror('Unsupported video format detected: %dx%d, using "%s".' %(vidwidth, vidheight, res))
            else:
                # too bad, we have an unusual format, try to make a sensible
                # guess depending on the par and hope for the best
                if par < 1.6:
                    # value picked rather randomly
                    res = '1440x1080'
                else:
                    res = '1080p'
                printerror('Unsupported video format detected: %dx%d (par: %f), using "%s".' %(vidwidth, vidheight, par, res))
            printerror('If the results are not satisfying, please try again with different parameters.')
            RESOLUTION = res
    except:
        printerror('Error while trying to detect video properties, exit.')
        sys.exit(1)

XSIZE, YSIZE = RES_PRESETS[RESOLUTION]

################   font options   #############

# do we have the requested font?
FONT = getfont(OPTS.FONT)

if not FONT:
    printerror('Unable to find font "%s", exit.' % OPTS.FONT)
    done(1)

MAXLINESIZE = OPTS.MAXLINESIZE
YOFFSET = int(round(YSIZE * OPTS.YOFFSET / 100))

OUTLINEWIDTH = OPTS.OUTLINEWIDTH
FONTSIZE = OPTS.FONTSIZE
if isinstance(FONTSIZE, float):
    FONTSIZE = int(round(FONTSIZE * YSIZE / 100.0))
if FONTSIZE >= YSIZE:
    printerror('Requested font size > resolution, this does not make any sense, exit.')
    done(1)

padx, pady = OPTS.BOXPADDING
if padx == -1:
    padx = int(round(FONTSIZE / 4))
if pady == -1:
    pady = int(round(FONTSIZE / 8))
BOXPADDING = (padx, pady)

######################   now do the work   #############################

####   create temp directory   ####

TEMPDIR = tempfile.mkdtemp(prefix='srt2vobsub', dir='/data/data/com.termux/files/usr/tmp')
os.chdir(TEMPDIR)

# try to convert the input file to UTF-8 first, so that we know what
# we're dealing with
name, ext = os.path.splitext(os.path.basename(INFILE))
f = open(INFILE, 'rb')
cont = f.read()
f.close()

ENCODING = OPTS.ENCODING
if ENCODING == 'auto':
    enc, conf = detect_encoding(INFILE)
    if enc:
        conf = str(round(conf * 100, 2))
        printmsg('Info: detected input file encoding %s (confidence: %s %%).' % (enc, conf))
        ENCODING = enc
    else:
        printerror('Error while trying to detect input file encoding.')
        printerror('Please set the input file encoding manually, exit.')
        done(1)

try:
    enc = ENCODING
    if ENCODING.lower() in ('utf-8', 'utf8'):
        # if the file contains a BOM srt.parse() would throw an error
        # if we use utf-8
        enc = 'utf-8-sig'
    cont = cont.decode(enc, errors=OPTS.ENCODINGERRORS)
    infile = os.path.join(TEMPDIR, '%s_tmp_utf8%s' % (name, ext))
    f = open(infile, mode='w', encoding='UTF-8')
    f.write(cont)
    f.close()
    ENCODING = 'UTF-8'
    INFILE = infile
except UnicodeError:
    printerror('Unable to open "%s" with encoding "%s".' % (INFILE, OPTS.ENCODING))
    printerror('Please choose a different encoding or try "--encodingerrors=replace".')
    done(1)

####   see if we have to convert the subtitles   ####

SRTFILE = None
if os.path.splitext(INFILE)[1].lower() == '.srt':
    f = open(INFILE, 'rb')
    cont = f.read()
    f.close()
    cont = cont.decode(ENCODING, errors=OPTS.ENCODINGERRORS)
    try:
        subs = list(srt.parse(cont))
        f = open(os.path.join(TEMPDIR, '%s.srt' % NAME), mode='w', encoding='UTF-8')
        f.write(cont)
        f.close()
        SRTFILE = os.path.join(TEMPDIR, '%s.srt' % NAME)
        ENCODING = 'UTF-8'
    except:
        # broken srt file or no srt file at all
        # still, take a chance and try if ffmpeg knows what to do with that
        pass

if not SRTFILE:
    SRTFILE = os.path.join(TEMPDIR, '%s.srt' % NAME)
    printmsg('Input file does not seem to be in srt format, converting...')
    ffmpeg = which('ffmpeg')
    if not ffmpeg:
        printerror('Could not find ffmpeg on your system, exit.')
        done(1)
    cmd = (ffmpeg, '-sub_charenc', ENCODING, '-i', INFILE, SRTFILE)
    if OPTS.VERBOSE == 2:
        print('Running command: %s\n' % ' '.join(cmd))
        sts = call(cmd)
    else:
        sts, out, err = getstatusoutput(cmd)
        if err:
            printerror(err)
    if sts:
        printerror('Error while trying to convert input file to srt.')
        printerror('Unsupported format or invalid content.')
        printerror('Maybe you tried to use the wrong encoding?')
        printerror('Converting input file to srt failed, exit.')
        done(sts)

####   try to parse the srt file   ####

try:
    f = open(SRTFILE, 'rb')
    cont = f.read()
    f.close()
    cont = cont.decode(ENCODING, errors=OPTS.ENCODINGERRORS)
    srt_gen = srt.parse(cont)
    subs1 = list(srt_gen)
except:
    msg = 'Unable to process file %s: invalid format.' % INFILE
    printerror(msg)
    done(1)

####   now process the subtitles one by one   ####

# by default sort_and_reindex() would also remove subtitles where endtime
# is before starttime,; since bdsup2sub seems to handle these well, we
# want to keep them however
srt.SUBTITLE_SKIP_CONDITIONS = (
    ("No content", lambda sub: not sub.content.strip()),
    ("Start time < 0 seconds", lambda sub: sub.start < ZERO_TIMEDELTA)
)
# if the subtitle order is messed in the srt file, fix it now
subs = list(srt.sort_and_reindex(subs1))

# sanity check of display time options
MINDISPLAYTIME = OPTS.MINDISPLAYTIME
MAXDISPLAYTIME = OPTS.MAXDISPLAYTIME
DEFAULTDISPLAYTIME = OPTS.DEFAULTDISPLAYTIME
if DEFAULTDISPLAYTIME < MINDISPLAYTIME:
    DEFAULTDISPLAYTIME = MINDISPLAYTIME
elif DEFAULTDISPLAYTIME > MAXDISPLAYTIME:
    DEFAULTDISPLAYTIME = MAXDISPLAYTIME

timedelta_defdtime = datetime.timedelta(seconds=DEFAULTDISPLAYTIME // 1000,
                                        milliseconds=DEFAULTDISPLAYTIME % 1000)
timedelta_mindtime = datetime.timedelta(seconds=MINDISPLAYTIME // 1000,
                                        milliseconds=MINDISPLAYTIME % 1000)
timedelta_maxdtime = datetime.timedelta(seconds=MAXDISPLAYTIME // 1000,
                                        milliseconds=MAXDISPLAYTIME % 1000)
timedelta_1ms = datetime.timedelta(milliseconds=1)

xml_events = []
first_intc = None
skipped = 0

for sub in subs:
    # remove blank lines and leading newlines
    caption = srt.make_legal_content(sub.content)
    # remove HI tags if requested
    if OPTS.REMOVE_HI != 'none':
        # HI tags may be enclosed in brackets or parentheses
        if 'b' in OPTS.REMOVE_HI:
            caption = re.sub('\[[^\[]+?\]', '', caption)
        if 'p' in OPTS.REMOVE_HI:
            caption = re.sub('\([^\(]+?\)', '', caption)
        # get rid of lines that are blank or only spaces
        caption = '\n'.join([x.strip() for x in caption.strip().splitlines() \
                                    if x.strip()])
        # remove lines that now contain only a solitary symbol
        caption = '\n'.join([x.strip() for x in caption.strip().splitlines() \
                                    if not x.strip() in '-#*♪'])
    # just to make sure:
    sub.content = srt.make_legal_content(caption)
    if not sub.content:
        skipped += 1

# check the subtitles again, in case some were removed
subs = list(srt.sort_and_reindex(subs))
printmsg('Info: found %d subtitles' % len(subs), verbosity=2)

for sub in subs:
    if OPTS.VERBOSE:
        sys.stdout.write('\rProcessing subtitle %d' % sub.index)
        sys.stdout.flush()
    filename = '%s_%s.png' % (NAME, str(sub.index).zfill(4))
    # remove html tags; do this here, so they are kept intact in
    # our corrected subtitles file
    caption = re.sub("<[^<]+?>", "", sub.content)
    # get rid of lines that are blank or only spaces
    caption = '\n'.join([x.strip() for x in caption.strip().splitlines() if x.strip()])
    if caption:
        # bdsup2sub might report an error if we try to export an empty image
        # here, so we better check if some text is left after stripping
        # the html
        width, height = export_caption(caption, filename)
    else:
        skipped += 1
    # try to fix srt timings that are impossibly short or long to a sane value
    t1 = sub.end
    # but do this only if requested by the user
    if OPTS.FIXTIMES:
        if (t1 - sub.start) < ZERO_TIMEDELTA:
            t1 = sub.start + timedelta_defdtime
        elif (t1 - sub.start) < timedelta_mindtime:
            t1 = sub.start + timedelta_mindtime
        elif (t1 - sub.start) > timedelta_maxdtime:
            t1 = sub.start + timedelta_maxdtime
        # check if the end time overlaps with the next subtitle's start time
        # (if any); if yes, correct this
        if subs[sub.index:]:
            if t1 >= subs[sub.index].start:
                t1 = subs[sub.index].start - timedelta_1ms

        if t1 != sub.end:
            printmsg('\nInfo: Corrected end time of subtitle %d from %s to %s' %(
                        sub.index,srt.timedelta_to_srt_timestamp(sub.end),
                        srt.timedelta_to_srt_timestamp(t1)), verbosity=2)

    sub.end = t1

    if caption:
        # finally create the xml string for this subtitle
        intc = timedelta2xmltime(sub.start)
        if first_intc is None:
            first_intc = intc
        outtc = timedelta2xmltime(t1)

        x = int(round((XSIZE - width) / 2))
        y = YSIZE - height - YOFFSET

        ev = '<Event InTC="%s" OutTC="%s" Forced="False">\n' % (intc, outtc)
        ev += '<Graphic Width="%d" Height="%d" X="%d" Y="%d">%s</Graphic>\n' % (
                                                        width, height, x, y, filename)
        ev += '</Event>\n'
        xml_events.append(ev)


printmsg('\rProcessed %d subtitles' % len(xml_events))
if skipped:
    printmsg('%d empty subtitle(s) were removed.' % skipped)

if OPTS.SAVESRT:
    try:
        cont = srt.compose(subs)
        fname = os.path.join(TEMPDIR, '%s_proc.srt' % NAME)
        f = open(fname, mode='w', encoding='UTF-8')
        f.write(cont)
        f.close()
        printmsg('Saved processed srt data to "%s".' % fname, verbosity=2)
    except:
        printerror('Error while trying to write "%s".' % fname)

####   put together the contents of the xml file and write it  ####

XML = '<?xml version="1.0" encoding="%s"?>\n' % ENCODING +\
'<BDN Version="0.93" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ' +\
'xsi:noNamespaceSchemaLocation="BD-03-006-0093b BDN File Format.xsd">\n' +\
'<Description>\n' +\
'<Name Title="%s" Content=""/>\n' % NAME +\
'<Language Code="und"/>\n'+\
'<Format VideoFormat="%s" FrameRate="%d" DropFrame="False"/>\n' % (RESOLUTION, FRAMERATE) +\
'<Events Type="Graphic" FirstEventInTC="%s" LastEventOutTC="%s" NumberofEvents="%d"/>\n' % (first_intc, outtc, len(xml_events)) +\
'</Description>\n<Events>\n'
for ev in xml_events:
    XML += ev
XML += '</Events>\n</BDN>\n'

xmlfile = '%s.xml' % NAME
f = open(xmlfile, 'w')
f.write(XML)
f.close()

####   now run bdsup2sub   ####

# video format from the xml file seems to be respected by bdsup2sub,
# language seems to be ignored however, so it needs to be set
# on the command line
# Update: with bdsup2sub v.5 language from xml is used if set to a proper
# 3-digit code like "eng" and is preferred over the language identifier
# given on the command line; so for consistent behavior with v.4 and v.5
# set it to something illegal like "und" in the xml which will then be ignored
# by both versions so that instead the 2-digit bdsup2sub lang codes can be used
BDSUP2SUBOPTS = OPTS.BDSUP2SUBOPTS

if BDSUP2SUBVERSION == '4':
    BDSUP2SUBOPTS += ('/lang:%s' % OPTS.LANGUAGE,)
    cmd = ('java', '-Xmx256m', '-jar', OPTS.BDSUP2SUBRUNTIME) +\
           BDSUP2SUBOPTS + (xmlfile, '%s.idx' % NAME)
else:
    BDSUP2SUBOPTS += ('-l', OPTS.LANGUAGE,)
    cmd = ('java', '-Xmx256m', '-jar', OPTS.BDSUP2SUBRUNTIME) +\
            BDSUP2SUBOPTS + ('-o', '%s.idx' % NAME, xmlfile)

printmsg('Generating vobsub files...')
printmsg('Running command: %s' % ' '.join(cmd), verbosity=2)

sts, out, err = getstatusoutput(cmd)
if OPTS.VERBOSE == 2:
    out = out.splitlines()
    for line in out:
        if line.startswith('#') or line.startswith('Decoding frame'):
            # bdsup2sub writes 2 lines for each caption, we don't want
            # to see that much output
            line = line + '\r'
        else:
            line = line + '\n'
        sys.stdout.write('    ' + line)
        sys.stdout.flush()
if err.strip():
    err = err.splitlines()
    printerror('bdsup2sub reported the following errors:')
    for line in err:
        printerror('    ' + line)

####   finally copy the vobsub files into the original subtitle's directory  ##

if sts:
    if BDSUP2SUBVERSION == '4':
        m = '(maybe you tried to run bdsup2sub v.5 with "-P 4" ?).'
    else:
        m = '(maybe you tried to run bdsup2sub v.4 with "-P 5" ?).'
    printerror('Error while trying to generate vobsub files ' + m)
else:
    res = 0
    res += save_file('%s.idx' % NAME, os.path.join(DIRECTORY, '%s.idx' % NAME))
    res += save_file('%s.sub' % NAME, os.path.join(DIRECTORY, '%s.sub' % NAME))
    if OPTS.SAVESRT:
        res += save_file('%s_proc.srt' % NAME, os.path.join(DIRECTORY, '%s_proc.srt' % NAME))
    if res == 0:
        etime = time.time()
        printmsg('Info: Generating subtitles took', int(round(etime-stime)), 'sec.', verbosity=2)
    else:
        printerror(
            'Error while trying to copy vobsub files to directory "%s".' % DIRECTORY)

################   done! :-)   ###################

done(sts)

