766 lines
29 KiB
Python
766 lines
29 KiB
Python
# -*- 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)
|
|
#
|
|
# 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
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# PyonFX 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 Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public License
|
|
# along with this program. If not, see http://www.gnu.org/licenses/.
|
|
|
|
from __future__ import annotations
|
|
import re
|
|
import math
|
|
import colorsys
|
|
from enum import Enum
|
|
from typing import List, NamedTuple, Tuple, Union, TYPE_CHECKING
|
|
|
|
from .font_utility import Font
|
|
|
|
if TYPE_CHECKING:
|
|
from .ass_core import Line, Word, Syllable, Char
|
|
from .shape import Shape
|
|
|
|
# A simple NamedTuple to represent pixels
|
|
Pixel = NamedTuple("Pixel", [("x", float), ("y", float), ("alpha", int)])
|
|
|
|
|
|
class ColorModel(Enum):
|
|
ASS = "&HBBGGRR&"
|
|
ASS_STYLE = "&HAABBGGRR"
|
|
RGB = "(r, g, b)"
|
|
RGB_STR = "#RRGGBB"
|
|
RGBA = "(r, g, b, a)"
|
|
RGBA_STR = "#RRGGBBAA"
|
|
HSV = "(h, s, v)"
|
|
|
|
|
|
class Convert:
|
|
"""
|
|
This class is a collection of static methods that will help
|
|
the user to convert everything needed to the ASS format.
|
|
"""
|
|
|
|
@staticmethod
|
|
def time(ass_ms: Union[int, str]) -> Union[str, int, ValueError]:
|
|
"""Converts between milliseconds and ASS timestamp.
|
|
|
|
You can probably ignore that function, you will not make use of it for KFX or typesetting generation.
|
|
|
|
Parameters:
|
|
ass_ms (int or str): If int, than milliseconds are expected, else ASS timestamp as str is expected.
|
|
|
|
Returns:
|
|
If milliseconds -> ASS timestamp, else if ASS timestamp -> milliseconds, else ValueError will be raised.
|
|
"""
|
|
# Milliseconds?
|
|
if type(ass_ms) is int and ass_ms >= 0:
|
|
return "{:d}:{:02d}:{:02d}.{:02d}".format(
|
|
math.floor(ass_ms / 3600000) % 10,
|
|
math.floor(ass_ms % 3600000 / 60000),
|
|
math.floor(ass_ms % 60000 / 1000),
|
|
math.floor(ass_ms % 1000 / 10),
|
|
)
|
|
# ASS timestamp?
|
|
elif type(ass_ms) is str and re.match(r"^\d:\d+:\d+\.\d+$", ass_ms):
|
|
return (
|
|
int(ass_ms[0]) * 3600000
|
|
+ int(ass_ms[2:4]) * 60000
|
|
+ int(ass_ms[5:7]) * 1000
|
|
+ int(ass_ms[8:10]) * 10
|
|
)
|
|
else:
|
|
raise ValueError("Milliseconds or ASS timestamp expected")
|
|
|
|
@staticmethod
|
|
def alpha_ass_to_dec(alpha_ass: str) -> int:
|
|
"""Converts from ASS alpha string to corresponding decimal value.
|
|
|
|
Parameters:
|
|
alpha_ass (str): A string in the format '&HXX&'.
|
|
|
|
Returns:
|
|
A decimal in [0, 255] representing ``alpha_ass`` converted.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.alpha_ass_to_dec("&HFF&"))
|
|
|
|
>>> 255
|
|
"""
|
|
try:
|
|
match = re.fullmatch(r"&H([0-9A-F]{2})&", alpha_ass)
|
|
return int(match.group(1), 16)
|
|
except TypeError as e:
|
|
raise TypeError(
|
|
f"Provided ASS alpha was expected of type 'str', but you provided a '{type(alpha_ass)}'."
|
|
) from e
|
|
except AttributeError as e:
|
|
raise ValueError(
|
|
f"Provided ASS alpha string '{alpha_ass}' is not in the expected format '&HXX&'."
|
|
) from e
|
|
|
|
@staticmethod
|
|
def alpha_dec_to_ass(alpha_dec: Union[int, float]) -> str:
|
|
"""Converts from decimal value to corresponding ASS alpha string.
|
|
|
|
Parameters:
|
|
alpha_dec (int or float): Decimal in [0, 255] representing an alpha value.
|
|
|
|
Returns:
|
|
A string in the format '&HXX&' representing ``alpha_dec`` converted.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.alpha_dec_to_ass(255))
|
|
print(Convert.alpha_dec_to_ass(255.0))
|
|
|
|
>>> "&HFF&"
|
|
>>> "&HFF&"
|
|
"""
|
|
try:
|
|
if not 0 <= alpha_dec <= 255:
|
|
raise ValueError(
|
|
f"Provided alpha decimal '{alpha_dec}' is out of the range [0, 255]."
|
|
)
|
|
except TypeError as e:
|
|
raise TypeError(
|
|
f"Provided alpha decimal was expected of type 'int' or 'float', but you provided a '{type(alpha_dec)}'."
|
|
) from e
|
|
return f"&H{round(alpha_dec):02X}&"
|
|
|
|
@staticmethod
|
|
def color(
|
|
c: Union[
|
|
str,
|
|
Union[
|
|
Tuple[
|
|
Union[int, float],
|
|
Union[int, float],
|
|
Union[int, float],
|
|
],
|
|
Tuple[
|
|
Union[int, float],
|
|
Union[int, float],
|
|
Union[int, float],
|
|
Union[int, float],
|
|
],
|
|
],
|
|
],
|
|
input_format: ColorModel,
|
|
output_format: ColorModel,
|
|
round_output: bool = True,
|
|
) -> Union[
|
|
str,
|
|
Tuple[int, int, int],
|
|
Tuple[int, int, int, int],
|
|
Tuple[float, float, float],
|
|
Tuple[float, float, float, float],
|
|
]:
|
|
"""Converts a provided color from a color model to another.
|
|
|
|
Parameters:
|
|
c (str or tuple of int or tuple of float): A color in the format ``input_format``.
|
|
input_format (ColorModel): The color format of ``c``.
|
|
output_format (ColorModel): The color format for the output.
|
|
round_output (bool): A boolean to determine whether the output should be rounded or not.
|
|
|
|
Returns:
|
|
A color in the format ``output_format``.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color("&H0000FF&", ColorModel.ASS, ColorModel.RGB))
|
|
|
|
>>> (255, 0, 0)
|
|
"""
|
|
try:
|
|
# Text for exception if input is out of ranges
|
|
input_range_e = f"Provided input '{c}' has value(s) out of the range "
|
|
|
|
# Parse input, obtaining its corresponding (r,g,b,a) values
|
|
if input_format == ColorModel.ASS:
|
|
match = re.fullmatch(r"&H([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})&", c)
|
|
(b, g, r), a = map(lambda x: int(x, 16), match.groups()), 255
|
|
elif input_format == ColorModel.ASS_STYLE:
|
|
match = re.fullmatch(
|
|
r"&H([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})", c
|
|
)
|
|
a, b, g, r = map(lambda x: int(x, 16), match.groups())
|
|
elif input_format == ColorModel.RGB:
|
|
if not all(0 <= n <= 255 for n in c):
|
|
raise ValueError(input_range_e + "[0, 255].")
|
|
(r, g, b), a = c, 255
|
|
elif input_format == ColorModel.RGB_STR:
|
|
match = re.fullmatch(r"#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})", c)
|
|
(r, g, b), a = map(lambda x: int(x, 16), match.groups()), 255
|
|
elif input_format == ColorModel.RGBA:
|
|
if not all(0 <= n <= 255 for n in c):
|
|
raise ValueError(input_range_e + "[0, 255].")
|
|
r, g, b, a = c
|
|
elif input_format == ColorModel.RGBA_STR:
|
|
match = re.fullmatch(
|
|
r"#([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})([0-9A-F]{2})", c
|
|
)
|
|
r, g, b, a = map(lambda x: int(x, 16), match.groups())
|
|
elif input_format == ColorModel.HSV:
|
|
if not (0 <= c[0] < 360 and 0 <= c[1] <= 100 and 0 <= c[2] <= 100):
|
|
raise ValueError(
|
|
input_range_e + "( [0, 360), [0, 100], [0, 100] )."
|
|
)
|
|
h, s, v = c[0] / 360, c[1] / 100, c[2] / 100
|
|
(r, g, b), a = map(lambda x: 255 * x, colorsys.hsv_to_rgb(h, s, v)), 255
|
|
except (AttributeError, ValueError, TypeError) as e:
|
|
# AttributeError -> re.fullmatch failed
|
|
# ValueError -> too many values to unpack
|
|
# TypeError -> in case the provided tuple is not a list of numbers
|
|
raise ValueError(
|
|
f"Provided input '{c}' is not in the format '{input_format}'."
|
|
) from e
|
|
|
|
# Convert (r,g,b,a) to the desired output_format
|
|
try:
|
|
if output_format == ColorModel.ASS:
|
|
return f"&H{round(b):02X}{round(g):02X}{round(r):02X}&"
|
|
elif output_format == ColorModel.ASS_STYLE:
|
|
return f"&H{round(a):02X}{round(b):02X}{round(g):02X}{round(r):02X}"
|
|
elif output_format == ColorModel.RGB:
|
|
method = round if round_output else float
|
|
return tuple(map(method, (r, g, b)))
|
|
elif output_format == ColorModel.RGB_STR:
|
|
return f"#{round(r):02X}{round(g):02X}{round(b):02X}"
|
|
elif output_format == ColorModel.RGBA:
|
|
method = round if round_output else float
|
|
return tuple(map(method, (r, g, b, a)))
|
|
elif output_format == ColorModel.RGBA_STR:
|
|
return f"#{round(r):02X}{round(g):02X}{round(b):02X}{round(a):02X}"
|
|
elif output_format == ColorModel.HSV:
|
|
method = round if round_output else float
|
|
h, s, v = colorsys.rgb_to_hsv(r / 255, g / 255, b / 255)
|
|
return method(h * 360) % 360, method(s * 100), method(v * 100)
|
|
else:
|
|
raise ValueError(f"Unsupported output_format ('{output_format}').")
|
|
except NameError as e:
|
|
raise ValueError(f"Unsupported input_format ('{input_format}').") from e
|
|
|
|
@staticmethod
|
|
def color_ass_to_rgb(
|
|
color_ass: str, as_str: bool = False
|
|
) -> Union[str, Tuple[int, int, int]]:
|
|
"""Converts from ASS color string to corresponding RGB color.
|
|
|
|
Parameters:
|
|
color_ass (str): A string in the format '&HBBGGRR&'.
|
|
as_str (bool): A boolean to determine the output type format.
|
|
|
|
Returns:
|
|
The output represents ``color_ass`` converted. If ``as_str`` = False, the output is a tuple of integers in range *[0, 255]*.
|
|
Else, the output is a string in the format '#RRGGBB'.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color_ass_to_rgb("&HABCDEF&"))
|
|
print(Convert.color_ass_to_rgb("&HABCDEF&", as_str=True))
|
|
|
|
>>> (239, 205, 171)
|
|
>>> "#EFCDAB"
|
|
"""
|
|
return Convert.color(
|
|
color_ass, ColorModel.ASS, ColorModel.RGB_STR if as_str else ColorModel.RGB
|
|
)
|
|
|
|
@staticmethod
|
|
def color_ass_to_hsv(
|
|
color_ass: str, round_output: bool = True
|
|
) -> Union[Tuple[int, int, int], Tuple[float, float, float]]:
|
|
"""Converts from ASS color string to corresponding HSV color.
|
|
|
|
Parameters:
|
|
color_ass (str): A string in the format '&HBBGGRR&'.
|
|
round_output (bool): A boolean to determine whether the output should be rounded or not.
|
|
|
|
Returns:
|
|
The output represents ``color_ass`` converted. If ``round_output`` = True, the output is a tuple of integers in range *( [0, 360), [0, 100], [0, 100] )*.
|
|
Else, the output is a tuple of floats in range *( [0, 360), [0, 100], [0, 100] )*.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color_ass_to_hsv("&HABCDEF&"))
|
|
print(Convert.color_ass_to_hsv("&HABCDEF&", round_output=False))
|
|
|
|
>>> (30, 28, 94)
|
|
>>> (30.000000000000014, 28.451882845188294, 93.72549019607843)
|
|
"""
|
|
return Convert.color(color_ass, ColorModel.ASS, ColorModel.HSV, round_output)
|
|
|
|
@staticmethod
|
|
def color_rgb_to_ass(
|
|
color_rgb: Union[
|
|
str, Tuple[Union[int, float], Union[int, float], Union[int, float]]
|
|
]
|
|
) -> str:
|
|
"""Converts from RGB color to corresponding ASS color.
|
|
|
|
Parameters:
|
|
color_rgb (str or tuple of int or tuple of float): Either a string in the format '#RRGGBB' or a tuple of three integers (or floats) in the range *[0, 255]*.
|
|
|
|
Returns:
|
|
A string in the format '&HBBGGRR&' representing ``color_rgb`` converted.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color_rgb_to_ass("#ABCDEF"))
|
|
|
|
>>> "&HEFCDAB&"
|
|
"""
|
|
return Convert.color(
|
|
color_rgb,
|
|
ColorModel.RGB_STR if type(color_rgb) is str else ColorModel.RGB,
|
|
ColorModel.ASS,
|
|
)
|
|
|
|
@staticmethod
|
|
def color_rgb_to_hsv(
|
|
color_rgb: Union[
|
|
str, Tuple[Union[int, float], Union[int, float], Union[int, float]]
|
|
],
|
|
round_output: bool = True,
|
|
) -> Union[Tuple[int, int, int], Tuple[float, float, float]]:
|
|
"""Converts from RGB color to corresponding HSV color.
|
|
|
|
Parameters:
|
|
color_rgb (str or tuple of int or tuple of float): Either a string in the format '#RRGGBB' or a tuple of three integers (or floats) in the range *[0, 255]*.
|
|
round_output (bool): A boolean to determine whether the output should be rounded or not.
|
|
|
|
Returns:
|
|
The output represents ``color_rgb`` converted. If ``round_output`` = True, the output is a tuple of integers in range *( [0, 360), [0, 100], [0, 100] )*.
|
|
Else, the output is a tuple of floats in range *( [0, 360), [0, 100], [0, 100] )*.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color_rgb_to_hsv("#ABCDEF"))
|
|
print(Convert.color_rgb_to_hsv("#ABCDEF"), round_output=False)
|
|
|
|
>>> (210, 28, 94)
|
|
>>> (210.0, 28.451882845188294, 93.72549019607843)
|
|
"""
|
|
return Convert.color(
|
|
color_rgb,
|
|
ColorModel.RGB_STR if type(color_rgb) is str else ColorModel.RGB,
|
|
ColorModel.HSV,
|
|
round_output,
|
|
)
|
|
|
|
@staticmethod
|
|
def color_hsv_to_ass(
|
|
color_hsv: Tuple[Union[int, float], Union[int, float], Union[int, float]]
|
|
) -> str:
|
|
"""Converts from HSV color string to corresponding ASS color.
|
|
|
|
Parameters:
|
|
color_hsv (tuple of int/float): A tuple of three integers (or floats) in the range *( [0, 360), [0, 100], [0, 100] )*.
|
|
|
|
Returns:
|
|
A string in the format '&HBBGGRR&' representing ``color_hsv`` converted.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color_hsv_to_ass((100, 100, 100)))
|
|
|
|
>>> "&H00FF55&"
|
|
"""
|
|
return Convert.color(color_hsv, ColorModel.HSV, ColorModel.ASS)
|
|
|
|
@staticmethod
|
|
def color_hsv_to_rgb(
|
|
color_hsv: Tuple[Union[int, float], Union[int, float], Union[int, float]],
|
|
as_str: bool = False,
|
|
round_output: bool = True,
|
|
) -> str:
|
|
"""Converts from HSV color string to corresponding RGB color.
|
|
|
|
Parameters:
|
|
color_hsv (tuple of int/float): A tuple of three integers (or floats) in the range *( [0, 360), [0, 100], [0, 100] )*.
|
|
as_str (bool): A boolean to determine the output type format.
|
|
round_output (bool): A boolean to determine whether the output should be rounded or not.
|
|
|
|
Returns:
|
|
The output represents ``color_hsv`` converted. If ``as_str`` = False, the output is a tuple
|
|
( also, if ``round_output`` = True, the output is a tuple of integers in range *( [0, 360), [0, 100], [0, 100] )*, else a tuple of float in range *( [0, 360), [0, 100], [0, 100] ) )*.
|
|
Else, the output is a string in the format '#RRGGBB'.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
print(Convert.color_hsv_to_rgb((100, 100, 100)))
|
|
print(Convert.color_hsv_to_rgb((100, 100, 100), as_str=True))
|
|
print(Convert.color_hsv_to_rgb((100, 100, 100), round_output=False))
|
|
|
|
>>> (85, 255, 0)
|
|
>>> "#55FF00"
|
|
>>> (84.99999999999999, 255.0, 0.0)
|
|
"""
|
|
return Convert.color(
|
|
color_hsv,
|
|
ColorModel.HSV,
|
|
ColorModel.RGB_STR if as_str else ColorModel.RGB,
|
|
round_output,
|
|
)
|
|
|
|
@staticmethod
|
|
def text_to_shape(
|
|
obj: Union[Line, Word, Syllable, Char], fscx: float = None, fscy: float = None
|
|
) -> Shape:
|
|
"""Converts text with given style information to an ASS shape.
|
|
|
|
**Tips:** *You can easily create impressive deforming effects.*
|
|
|
|
Parameters:
|
|
obj (Line, Word, Syllable or Char): An object of class Line, Word, Syllable or Char.
|
|
fscx (float, optional): The scale_x value for the shape.
|
|
fscy (float, optional): The scale_y value for the shape.
|
|
|
|
Returns:
|
|
A Shape object, representing the text with the style format values of the object.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
line = Line.copy(lines[1])
|
|
line.text = "{\\\\an7\\\\pos(%.3f,%.3f)\\\\p1}%s" % (line.left, line.top, Convert.text_to_shape(line))
|
|
io.write_line(line)
|
|
"""
|
|
# Obtaining information and editing values of style if requested
|
|
original_scale_x = obj.styleref.scale_x
|
|
original_scale_y = obj.styleref.scale_y
|
|
|
|
# Editing temporary the style to properly get the shape
|
|
if fscx is not None:
|
|
obj.styleref.scale_x = fscx
|
|
if fscy is not None:
|
|
obj.styleref.scale_y = fscy
|
|
|
|
# Obtaining font information from style and obtaining shape
|
|
font = Font(obj.styleref)
|
|
shape = font.text_to_shape(obj.text)
|
|
# Clearing resources to not let overflow errors take over
|
|
del font
|
|
|
|
# Restoring values of style and returning the shape converted
|
|
if fscx is not None:
|
|
obj.styleref.scale_x = original_scale_x
|
|
if fscy is not None:
|
|
obj.styleref.scale_y = original_scale_y
|
|
return shape
|
|
|
|
@staticmethod
|
|
def text_to_clip(
|
|
obj: Union[Line, Word, Syllable, Char],
|
|
an: int = 5,
|
|
fscx: float = None,
|
|
fscy: float = None,
|
|
) -> Shape:
|
|
"""Converts text with given style information to an ASS shape, applying some translation/scaling to it since
|
|
it is not possible to position a shape with \\pos() once it is in a clip.
|
|
|
|
This is an high level function since it does some additional operations, check text_to_shape for further infromations.
|
|
|
|
**Tips:** *You can easily create text masks even for growing/shrinking text without too much effort.*
|
|
|
|
Parameters:
|
|
obj (Line, Word, Syllable or Char): An object of class Line, Word, Syllable or Char.
|
|
an (integer, optional): The alignment wanted for the shape.
|
|
fscx (float, optional): The scale_x value for the shape.
|
|
fscy (float, optional): The scale_y value for the shape.
|
|
|
|
Returns:
|
|
A Shape object, representing the text with the style format values of the object.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
line = Line.copy(lines[1])
|
|
line.text = "{\\\\an5\\\\pos(%.3f,%.3f)\\\\clip(%s)}%s" % (line.center, line.middle, Convert.text_to_clip(line), line.text)
|
|
io.write_line(line)
|
|
"""
|
|
# Checking for errors
|
|
if an < 1 or an > 9:
|
|
raise ValueError("Alignment value must be an integer between 1 and 9")
|
|
|
|
# Setting default values
|
|
if fscx is None:
|
|
fscx = obj.styleref.scale_x
|
|
if fscy is None:
|
|
fscy = obj.styleref.scale_y
|
|
|
|
# Obtaining text converted to shape
|
|
shape = Convert.text_to_shape(obj, fscx, fscy)
|
|
|
|
# Setting mult_x based on alignment
|
|
if an % 3 == 1: # an=1 or an=4 or an=7
|
|
mult_x = 0
|
|
elif an % 3 == 2: # an=2 or an=5 or an=8
|
|
mult_x = 1 / 2
|
|
else:
|
|
mult_x = 1
|
|
|
|
# Setting mult_y based on alignment
|
|
if an < 4:
|
|
mult_y = 1
|
|
elif an < 7:
|
|
mult_y = 1 / 2
|
|
else:
|
|
mult_y = 0
|
|
|
|
# Calculating offsets
|
|
cx = (
|
|
obj.left
|
|
- obj.width * mult_x * (fscx - obj.styleref.scale_x) / obj.styleref.scale_x
|
|
)
|
|
cy = (
|
|
obj.top
|
|
- obj.height * mult_y * (fscy - obj.styleref.scale_y) / obj.styleref.scale_y
|
|
)
|
|
|
|
return shape.move(cx, cy)
|
|
|
|
@staticmethod
|
|
def text_to_pixels(
|
|
obj: Union[Line, Word, Syllable, Char], supersampling: int = 8
|
|
) -> List[Pixel]:
|
|
"""| Converts text with given style information to a list of pixel data.
|
|
| A pixel data is a dictionary containing 'x' (horizontal position), 'y' (vertical position) and 'alpha' (alpha/transparency).
|
|
|
|
It is highly suggested to create a dedicated style for pixels,
|
|
because you will write less tags for line in your pixels, which means less size for your .ass file.
|
|
|
|
| The style suggested is:
|
|
| - **an=7 (very important!);**
|
|
| - bord=0;
|
|
| - shad=0;
|
|
| - For Font informations leave whatever the default is;
|
|
|
|
**Tips:** *It allows easy creation of text decaying or light effects.*
|
|
|
|
Parameters:
|
|
obj (Line, Word, Syllable or Char): An object of class Line, Word, Syllable or Char.
|
|
supersampling (int): Value used for supersampling. Higher value means smoother and more precise anti-aliasing (and more computational time for generation).
|
|
|
|
Returns:
|
|
A list of dictionaries representing each individual pixel of the input text styled.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
line = lines[2].copy()
|
|
line.style = "p"
|
|
p_sh = Shape.rectangle()
|
|
for pixel in Convert.text_to_pixels(line):
|
|
x, y = math.floor(line.left) + pixel['x'], math.floor(line.top) + pixel['y']
|
|
alpha = "\\alpha" + Convert.color_alpha_to_ass(pixel['alpha']) if pixel['alpha'] != 255 else ""
|
|
|
|
line.text = "{\\p1\\pos(%d,%d)%s}%s" % (x, y, alpha, p_sh)
|
|
io.write_line(line)
|
|
"""
|
|
shape = Convert.text_to_shape(obj).move(obj.left % 1, obj.top % 1)
|
|
return Convert.shape_to_pixels(shape, supersampling)
|
|
|
|
@staticmethod
|
|
def shape_to_pixels(shape: Shape, supersampling: int = 8) -> List[Pixel]:
|
|
"""| Converts a Shape object to a list of pixel data.
|
|
| A pixel data is a dictionary containing 'x' (horizontal position), 'y' (vertical position) and 'alpha' (alpha/transparency).
|
|
|
|
It is highly suggested to create a dedicated style for pixels,
|
|
because you will write less tags for line in your pixels, which means less size for your .ass file.
|
|
|
|
| The style suggested is:
|
|
| - **an=7 (very important!);**
|
|
| - bord=0;
|
|
| - shad=0;
|
|
| - For Font informations leave whatever the default is;
|
|
|
|
**Tips:** *As for text, even shapes can decay!*
|
|
|
|
Parameters:
|
|
shape (Shape): An object of class Shape.
|
|
supersampling (int): Value used for supersampling. Higher value means smoother and more precise anti-aliasing (and more computational time for generation).
|
|
|
|
Returns:
|
|
A list of dictionaries representing each individual pixel of the input shape.
|
|
|
|
Examples:
|
|
.. code-block:: python3
|
|
|
|
line = lines[2].copy()
|
|
line.style = "p"
|
|
p_sh = Shape.rectangle()
|
|
for pixel in Convert.shape_to_pixels(Shape.heart(100)):
|
|
# Random circle to pixel effect just to show
|
|
x, y = math.floor(line.left) + pixel.x, math.floor(line.top) + pixel.y
|
|
alpha = "\\alpha" + Convert.color_alpha_to_ass(pixel.alpha) if pixel.alpha != 255 else ""
|
|
|
|
line.text = "{\\p1\\pos(%d,%d)%s\\fad(0,%d)}%s" % (x, y, alpha, l.dur/4, p_sh)
|
|
io.write_line(line)
|
|
"""
|
|
# Scale values for supersampled rendering
|
|
upscale = supersampling
|
|
downscale = 1 / upscale
|
|
|
|
# Upscale shape for later downsampling
|
|
shape.map(lambda x, y: (x * upscale, y * upscale))
|
|
|
|
# Bring shape near origin in positive room
|
|
x1, y1, x2, y2 = shape.bounding()
|
|
shift_x, shift_y = -1 * (x1 - x1 % upscale), -1 * (y1 - y1 % upscale)
|
|
shape.move(shift_x, shift_y)
|
|
|
|
# Create image
|
|
width, height = (
|
|
math.ceil((x2 + shift_x) * downscale) * upscale,
|
|
math.ceil((y2 + shift_y) * downscale) * upscale,
|
|
)
|
|
image = [False for i in range(width * height)]
|
|
|
|
# Renderer (on binary image with aliasing)
|
|
lines, last_point, last_move = [], {}, {}
|
|
|
|
def collect_lines(x, y, typ):
|
|
# Collect lines (points + vectors)
|
|
nonlocal lines, last_point, last_move
|
|
x, y = int(round(x)), int(round(y)) # Use integers to avoid rounding errors
|
|
|
|
# Move
|
|
if typ == "m":
|
|
# Close figure with non-horizontal line in image
|
|
if (
|
|
last_move
|
|
and last_move["y"] != last_point["y"]
|
|
and not (last_point["y"] < 0 and last_move["y"] < 0)
|
|
and not (last_point["y"] > height and last_move["y"] > height)
|
|
):
|
|
lines.append(
|
|
[
|
|
last_point["x"],
|
|
last_point["y"],
|
|
last_move["x"] - last_point["x"],
|
|
last_move["y"] - last_point["y"],
|
|
]
|
|
)
|
|
|
|
last_move = {"x": x, "y": y}
|
|
# Non-horizontal line in image
|
|
elif (
|
|
last_point
|
|
and last_point["y"] != y
|
|
and not (last_point["y"] < 0 and y < 0)
|
|
and not (last_point["y"] > height and y > height)
|
|
):
|
|
lines.append(
|
|
[
|
|
last_point["x"],
|
|
last_point["y"],
|
|
x - last_point["x"],
|
|
y - last_point["y"],
|
|
]
|
|
)
|
|
|
|
# Remember last point
|
|
last_point = {"x": x, "y": y}
|
|
|
|
shape.flatten().map(collect_lines)
|
|
|
|
# Close last figure with non-horizontal line in image
|
|
if (
|
|
last_move
|
|
and last_move["y"] != last_point["y"]
|
|
and not (last_point["y"] < 0 and last_move["y"] < 0)
|
|
and not (last_point["y"] > height and last_move["y"] > height)
|
|
):
|
|
lines.append(
|
|
[
|
|
last_point["x"],
|
|
last_point["y"],
|
|
last_move["x"] - last_point["x"],
|
|
last_move["y"] - last_point["y"],
|
|
]
|
|
)
|
|
|
|
# Calculates line x horizontal line intersection
|
|
def line_x_hline(x, y, vx, vy, y2):
|
|
if vy != 0:
|
|
s = (y2 - y) / vy
|
|
if s >= 0 and s <= 1:
|
|
return x + s * vx
|
|
return None
|
|
|
|
# Scan image rows in shape
|
|
_, y1, _, y2 = shape.bounding()
|
|
for y in range(max(math.floor(y1), 0), min(math.ceil(y2), height)):
|
|
# Collect row intersections with lines
|
|
row_stops = []
|
|
for line in lines:
|
|
cx = line_x_hline(line[0], line[1], line[2], line[3], y + 0.5)
|
|
if cx is not None:
|
|
row_stops.append(
|
|
[max(0, min(cx, width)), 1 if line[3] > 0 else -1]
|
|
) # image trimmed stop position & line vertical direction
|
|
|
|
# Enough intersections / something to render?
|
|
if len(row_stops) > 1:
|
|
# Sort row stops by horizontal position
|
|
row_stops.sort(key=lambda x: x[0])
|
|
# Render!
|
|
status, row_index = 0, y * width
|
|
for i in range(0, len(row_stops) - 1):
|
|
status = status + row_stops[i][1]
|
|
if status != 0:
|
|
for x in range(
|
|
math.ceil(row_stops[i][0] - 0.5),
|
|
math.floor(row_stops[i + 1][0] + 0.5),
|
|
):
|
|
image[row_index + x] = True
|
|
|
|
# Extract pixels from image
|
|
pixels = []
|
|
for y in range(0, height, upscale):
|
|
for x in range(0, width, upscale):
|
|
opacity = 0
|
|
for yy in range(0, upscale):
|
|
for xx in range(0, upscale):
|
|
if image[(y + yy) * width + (x + xx)]:
|
|
opacity = opacity + 255
|
|
|
|
if opacity > 0:
|
|
pixels.append(
|
|
Pixel(
|
|
x=(x - shift_x) * downscale,
|
|
y=(y - shift_y) * downscale,
|
|
alpha=round(opacity * downscale ** 2),
|
|
)
|
|
)
|
|
|
|
return pixels
|
|
|
|
@staticmethod
|
|
def image_to_ass(image):
|
|
pass
|
|
|
|
@staticmethod
|
|
def image_to_pixels(image):
|
|
pass
|