inital commit
This commit is contained in:
15 files changed
+711
No files matched your search
@@ -0,0 +1,70 @@
|
||||
"""Read and write frame-timed Flipnote Subtitle files. No dependencies."""
|
||||
import re
|
||||
|
||||
MAX_FILE_BYTES = 1024 * 1024
|
||||
MAX_PAGE = 2**53 - 1
|
||||
|
||||
|
||||
class FsrtError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def _normalise(cues, max_page, max_cues):
|
||||
if type(max_page) is not int or not 1 <= max_page <= MAX_PAGE or type(max_cues) is not int or max_cues < 1:
|
||||
raise FsrtError('Page and subtitle limits must be positive integers.')
|
||||
if not isinstance(cues, list) or not cues:
|
||||
raise FsrtError('The file has no subtitles.')
|
||||
if len(cues) > max_cues:
|
||||
raise FsrtError(f'Use no more than {max_cues} subtitles.')
|
||||
result = []
|
||||
for index, cue in enumerate(cues, 1):
|
||||
cue = cue if isinstance(cue, dict) else {}
|
||||
start, end, text = cue.get('start'), cue.get('end'), cue.get('text')
|
||||
if type(start) is not int or type(end) is not int or not 1 <= start <= end <= min(max_page, MAX_PAGE):
|
||||
raise FsrtError(f'Subtitle {index}: choose pages between 1 and {max_page}, with the end on or after the start.')
|
||||
if not isinstance(text, str):
|
||||
raise FsrtError(f'Subtitle {index}: enter some text.')
|
||||
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
||||
if not text.strip() or re.search(r'[\x00-\x08\x0b-\x1f\x7f]', text) or any(not line.strip() for line in text.split('\n')):
|
||||
raise FsrtError(f'Subtitle {index}: text must not contain blank lines or control characters.')
|
||||
result.append({'start': start, 'end': end, 'text': text})
|
||||
result.sort(key=lambda cue: (cue['start'], cue['end']))
|
||||
if any(b['start'] <= a['end'] for a, b in zip(result, result[1:])):
|
||||
raise FsrtError('Two subtitles overlap. End one before the next starts.')
|
||||
return result
|
||||
|
||||
|
||||
def _size_check(text):
|
||||
try:
|
||||
size = len(text.encode('utf-8'))
|
||||
except UnicodeEncodeError:
|
||||
raise FsrtError('Subtitle text must be valid UTF-8.') from None
|
||||
if size > MAX_FILE_BYTES:
|
||||
raise FsrtError('The subtitle file must be 1 MiB or smaller.')
|
||||
|
||||
|
||||
def parse(text, *, max_page=MAX_PAGE, max_cues=10000):
|
||||
if not isinstance(text, str):
|
||||
raise FsrtError('Read the file as UTF-8 text.')
|
||||
_size_check(text)
|
||||
value = text.removeprefix('\ufeff').replace('\r\n', '\n').replace('\r', '\n')
|
||||
value = re.sub(r'^(?:[ \t]*\n)+|(?:\n[ \t]*)+$', '', value)
|
||||
if not value.strip():
|
||||
raise FsrtError('The file has no subtitles.')
|
||||
cues = []
|
||||
for index, block in enumerate(re.split(r'\n(?:[ \t]*\n)+', value), 1):
|
||||
lines = block.split('\n')
|
||||
if not re.fullmatch(r'[ \t]*[0-9]{1,16}[ \t]*', lines[0]) or not 1 <= int(lines[0]) <= MAX_PAGE:
|
||||
raise FsrtError(f'Subtitle {index}: put a positive sequence number on the first line.')
|
||||
match = re.fullmatch(r'[ \t]*([0-9]{1,16})[ \t]*-->[ \t]*([0-9]{1,16})[ \t]*', lines[1] if len(lines) > 1 else '')
|
||||
if not match:
|
||||
raise FsrtError(f'Subtitle {index}: use page numbers such as 1 --> 12, instead of timestamps.')
|
||||
cues.append({'start': int(match[1]), 'end': int(match[2]), 'text': '\n'.join(lines[2:])})
|
||||
return _normalise(cues, max_page, max_cues)
|
||||
|
||||
|
||||
def stringify(cues, *, max_page=MAX_PAGE, max_cues=10000):
|
||||
values = _normalise(cues, max_page, max_cues)
|
||||
text = '\n\n'.join(f"{index}\n{cue['start']} --> {cue['end']}\n{cue['text']}" for index, cue in enumerate(values, 1)) + '\n'
|
||||
_size_check(text)
|
||||
return text
|
||||
Reference in new issue
Block a user
