Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

0.9.10 reloaded - load support for file and in memory content #32

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions examples/1 - Basics/0 - Dump default ASS values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
This script visualizes which ASS values you got from input ASS file.

First of all you need to create an Ass object, which will help you to manage
input/output. Once created, it will automatically extract all the informations
from the input .ass file.

For more info about the use of Ass class:
https://pyonfx.readthedocs.io/en/latest/reference/ass%20core.html#pyonfx.ass_core.Ass

By executing this script, you'll discover how ASS contents,
like video resolution, styles, lines etc. are stored into objects and lists.
It's important to understand it, because these Python lists and objects
are exactly the values you'll be working with the whole time to create KFX.

Don't worry about the huge output, there are a lot of information
even in a small input file like the one in this folder.

You can find more info about each object used to represent the input .ass file here:
https://pyonfx.readthedocs.io/en/latest/reference/ass%20core.html
"""
from pyonfx import *

io = Ass() #With no args and no load after...

#...io.path_input will be set so...
stream = open(io.path_input, "r", encoding="utf-8-sig")
content=stream.read()
stream.close()
#....we will have default Aegisub Untitled.ass content file
print(content)
print("▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀")
#Let's have some fun, content end matches with first line sub as no CR so..
content=content+"PyonFX reloaded rocks!"
print(content)
io.load(content)

print("▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀")

meta, styles, lines = io.get_data()
print(meta)
print("▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀")
print(styles)
print("▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀")
print(lines)
2 changes: 1 addition & 1 deletion examples/1 - Basics/1 - Look into ASS values.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"""
from pyonfx import *

io = Ass("in.ass")
io = Ass("in.ass") #equivalent to io = Ass(); io.load("in.ass")
meta, styles, lines = io.get_data()

print(meta)
Expand Down
18 changes: 18 additions & 0 deletions pyonfx/Untitled.ass
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[Script Info]
; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/
Title: Default Aegisub file
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: None

[Aegisub Project Garbage]

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,
22 changes: 22 additions & 0 deletions pyonfx/Untitled_dummy.ass
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[Script Info]
; Script generated by Aegisub 3.2.2
; http://www.aegisub.org/
Title: Default Aegisub file
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
YCbCr Matrix: None
PlayResX: 640
PlayResY: 480

[Aegisub Project Garbage]
Video File: ?dummy:23.976000:40000:640:480:47:163:254:
Video AR Value: 1.333333

[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1

[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: 0,0:00:00.00,0:00:05.00,Default,,0,0,0,,
2 changes: 1 addition & 1 deletion pyonfx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@
from .shape import Shape
from .utils import Utils, FrameUtility, ColorUtility

__version__ = "0.9.10"
__version__ = "0.9.10-reloaded"
178 changes: 156 additions & 22 deletions pyonfx/ass_core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# PyonFX: An easy way to create KFX (Karaoke Effects) and complex typesetting using the ASS format (Advanced Substation Alpha).
# Copyright (C) 2019 Antonio Strippoli (CoffeeStraw/YellowFlash)
# Copyright (C) 2021 SoSie-js (sos-productions.com)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
Expand Down Expand Up @@ -386,7 +387,7 @@ class Ass:
Additionally, ``line`` fields will be re-calculated based on the re-positioned ``line.chars``.

Attributes:
path_input (str): Path for input file (absolute).
path_input (str): Path for input file (absolute). If none create default from scratch like in aegisub Untitled.ass
path_output (str): Path for output file (absolute).
meta (:class:`Meta`): Contains informations about the ASS given.
styles (list of :class:`Style`): Contains all the styles in the ASS given.
Expand All @@ -396,49 +397,153 @@ class Ass:
Example:
.. code-block:: python3

io = Ass("in.ass")
io = Ass ("in.ass")
meta, styles, lines = io.get_data()
"""

def __init__(
def __init__( self,
path_input: str = "",
path_output: str = "Output.ass",
keep_original: bool = True,
extended: bool = True,
vertical_kanji: bool = False,):
# Starting to take process time
self.__saved = False
self.__plines = 0
self.__ptime = time.time()
self.meta, self.styles, self.lines = Meta(), {}, []

#if(path_input != ""):
# print("Warning path input is ignored, please use input() or load()")
#self.input(path_input)

self.__output = []
self.__output_extradata = []

self.parse_ass(path_input, path_output, keep_original, extended, vertical_kanji)

def set_input(self, path_input) :
"""
Allow to set the input file
Args:
path_input (str): Path for the input file (either relative to your .py file or absolute).
"""
section_pattern = re.compile(r"^\[Script Info\]")
if(path_input == ""):
#Use aesisub default template
path_input=os.path.join(os.path.dirname(os.path.abspath(__file__)),"Untitled.ass")
elif section_pattern.match(path_input):
#path input is an ass valid content
pass
else:
# Getting absolute sub file path
dirname = os.path.dirname(os.path.abspath(sys.argv[0]))

if(re.search("\.(ass|ssa|jass|jsos)$",path_input)):
if not os.path.isabs(path_input):
path_input = os.path.join(dirname, path_input)

# Checking sub file validity (does it exists?)
if not os.path.isfile(path_input):
raise FileNotFoundError(
"Invalid path for the Subtitle file: %s" % path_input
)
else:
raise FileNotFoundError(
"Invalid input for the Subtitle file"
)
self.path_input = path_input

def set_output(self, path_output) :
"""
Allow to set the output file
Args:
path_output (str): Path for the output file (either relative to your .py file or absolute)
"""
# Getting absolute sub file path
dirname = os.path.dirname(os.path.abspath(sys.argv[0]))
# Getting absolute output file path
if path_output == "Output.ass" or path_output == "Untitled.ass":
path_output = os.path.join(dirname, path_output)
elif not os.path.isabs(path_output):
path_output = os.path.join(dirname, path_output)

self.path_output = path_output

def parse_ass(
self,
path_input: str = "",
path_output: str = "Output.ass",
keep_original: bool = True,
extended: bool = True,
vertical_kanji: bool = False,
):
"""
Parse an input ASS file using its path
Args:
path_input (str): Path for the input file (either relative to your .py file or absolute).
path_output (str): Path for the output file (either relative to your .py file or absolute) (DEFAULT: "Output.ass").
keep_original (bool): If True, you will find all the lines of the input file commented before the new lines generated.
extended (bool): Calculate more informations from lines (usually you will not have to touch this).
vertical_kanji (bool): If True, line text with alignment 4, 5 or 6 will be positioned vertically.
Additionally, ``line`` fields will be re-calculated based on the re-positioned ``line.chars``.

Attributes:
path_input (str): Path for input file (absolute). If none create default from scratch like in aegisub Untitled.ass
path_output (str): Path for output file (absolute).
meta (:class:`Meta`): Contains informations about the ASS given.
styles (list of :class:`Style`): Contains all the styles in the ASS given.
lines (list of :class:`Line`): Contains all the lines (events) in the ASS given.

.. _example:
Example:
.. code-block:: python3

io = Ass ("in.ass")
meta, styles, lines = io.get_data()
"""
# Starting to take process time
self.__saved = False
self.__plines = 0
self.__ptime = time.time()

self.meta, self.styles, self.lines = Meta(), {}, []
# Getting absolute sub file path
dirname = os.path.dirname(os.path.abspath(sys.argv[0]))
if not os.path.isabs(path_input):
path_input = os.path.join(dirname, path_input)

# Checking sub file validity (does it exists?)
if not os.path.isfile(path_input):
raise FileNotFoundError(
"Invalid path for the Subtitle file: %s" % path_input
)
content=""
section_pattern = re.compile(r"^\[Script Info\]")
if(section_pattern.match(path_input)):
# input is a content
content = path_input
elif(path_input ==""):
dirname = os.path.dirname(__file__)
path_input = os.path.join(dirname, "Untitled.ass")
else:
# input is a path file
self.set_input(path_input)
path_input =self.path_input

# Getting absolute output file path
if path_output == "Output.ass":
path_output = os.path.join(dirname, path_output)
elif not os.path.isabs(path_output):
path_output = os.path.join(dirname, path_output)
self.set_output(path_output)

self.path_input = path_input
self.path_output = path_output
self.__output = []
self.__output_extradata = []

section = ""
li = 0
for line in open(self.path_input, "r", encoding="utf-8-sig"):

#Get the stream of content or file content
if(content):
from io import StringIO
stream = StringIO(content)
else:
try:
stream = open(path_input, "r", encoding="utf-8-sig")
except FileNotFoundError:
raise FileNotFoundError(
"Unsupported or broken subtitle file: '%s'" % path_input
)
#previous read set the cursor at the end, put it back at the start
stream.seek(0,0)
for line in stream:
# Getting section
section_pattern = re.compile(r"^\[([^\]]*)")
if section_pattern.match(line):
Expand Down Expand Up @@ -584,7 +689,18 @@ def get_media_abs_path(mediafile):
# Adding informations to lines and meta?
if not extended:
return None
else:
return self.add_pyonfx_extension()

def add_pyonfx_extension(self):
"""
Calculate more informations from lines ( this affects the lines only if play_res_x and play_res_y are provided in the Script info section).
Args: None
return None
"""
#security check if no video provided, abort calculations
if (not hasattr(self.meta,"play_res_x") or not hasattr(self.meta,"play_res_y")):
return None
lines_by_styles = {}
# Let the fun begin (Pyon!)
for li, line in enumerate(self.lines):
Expand Down Expand Up @@ -1123,10 +1239,28 @@ def get_media_abs_path(mediafile):
def get_data(self) -> Tuple[Meta, Style, List[Line]]:
"""Utility function to retrieve easily meta styles and lines.

Returns:
:attr:`meta`, :attr:`styles` and :attr:`lines`
Returns:
:attr:`meta`, :attr:`styles` and :attr:`lines`
"""
return self.meta, self.styles, self.lines

def del_line(self,no):
"""Delete a line of the output list (which is private) """
nb=-1

# Retrieve the index of the first line, this is ugly having to do so
#as is if we could'nt rectify self.lines instead and generate self.__output on save()
# in lua this is what has been done when you get the aegisub object
for li, line in enumerate(self.__output):
if re.match(r"\n?(Dialogue|Comment): (.+?)$", line):
nb=li-1
break;

if (nb >=0) and isinstance(self.__output[no+nb], str):
del self.__output[no+nb]
self.__plines -= 1
else:
raise TypeError("No Line %d exists" % no)

def write_line(self, line: Line) -> Optional[TypeError]:
"""Appends a line to the output list (which is private) that later on will be written to the output file when calling save().
Expand Down