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

Letters findObjects alternative, can effectively solve the problem of letter splitting #1251

Closed
clotyxf opened this issue Jun 29, 2020 · 2 comments
Labels
question Questions regarding functionality, usage

Comments

@clotyxf
Copy link

clotyxf commented Jun 29, 2020

imagemagick_tools.py

import os
import re
import tempfile
import subprocess as sp
from PIL import Image

IMAGEMAGICK_BINARY = "convert"


def subprocess_call(cmd, stdout=sp.PIPE, stderr=sp.PIPE):
    """
    Parameters
    ------------------
    cmd
        list

    Returns
    -----------------
    mixed
    """

    popen_params = {
        "bufsize": 10 ** 5,
        "stdout": stdout,
        "stderr": stderr,
        "stdin": sp.DEVNULL,
    }

    if os.name == "nt":
        popen_params["creationflags"] = 0x08000000

    proc = sp.Popen(cmd, **popen_params)

    out, err = proc.communicate()

    if proc.returncode and err is not None:
        raise IOError(err.decode("utf8"))

    del proc

    return out, err

def txt_to_image(
    txt,
    font,
    fontsize=None,
    color="black",
    bg_color="transparent",
    align="center",
    kerning=None,
    stroke_color=None,
    stroke_width=1,
    size=None,
    interline=None,
    method="label",
    blur=None,
    tempfilename=None,
    debug=False,
    print_cmd=False,
):

    cmd = [IMAGEMAGICK_BINARY]

    if debug:
        cmd += ["-debug", "annotate"]

    cmd += ["-background", bg_color, "-fill", color, "-font", font]

    if fontsize is not None and fontsize > 0:
        cmd += ["-pointsize", "%d" % fontsize]

    if kerning is not None:
        cmd += ["-kerning", "%0.1f" % kerning]

    if stroke_color is not None:
        cmd += ["-stroke", stroke_color, "-strokewidth", "%.01f" % stroke_width]

    if size is not None:
        w = '' if size[0] is None or size[0] == -1 else size[0]
        h = '' if size[1] is None or size[1] == -1 else size[1]

        cmd += ["-size", "%sx%s" % (w, h)]

    if blur is not None:
        cmd += ["-blur", "%sx%s" % (blur[0], blur[1])]  # -gaussian-blur

    if align is not None:
        cmd += ["-gravity", align]

    if interline is not None:
        cmd += ["-interline-spacing", "%d" % interline]

    if tempfilename is None:
        tempfile_fd, tempfilename = tempfile.mkstemp(suffix=".png")
        os.close(tempfile_fd)

    temptxt_fd, temptxt = tempfile.mkstemp(suffix=".txt")

    try:  # only in Python3 will this work
        os.write(temptxt_fd, bytes(txt, "UTF8"))
    except TypeError:  # oops, fall back to Python2
        os.write(temptxt_fd, txt)

    os.close(temptxt_fd)
    txt = "@" + temptxt

    cmd += [
        "%s:%s" % (method, txt),
        "-type",
        "truecolormatte",
        "PNG32:%s" % tempfilename,
    ]

    if print_cmd:
        print(" ".join(cmd))

    try:
        _, error = (
            subprocess_call(cmd)
            if debug
            else subprocess_call(cmd=cmd, stdout=None, stderr=None)
        )
    except (IOError, OSError) as err:
        error = "ImageMagick Error: creation of failed because of the " "following error:\n\n%s.\n\n." % (
            str(err)
        ) + (
            "This error can be due to the fact that ImageMagick "
            "is not installed on your computer, or (for Windows "
            "users) that you didn't specify the path to the "
            "ImageMagick binary in file conf.py, or that the path "
            "you specified is incorrect"
        )
        raise IOError(error)

    os.remove(temptxt)

    if debug:
        error = None if error is None else error.decode("utf-8")
        return tempfilename, error

    return tempfilename


def get_font_metrics(
    txt,
    font,
    fontsize=None,
    color="black",
    align="center",
    kerning=None,
    stroke_color=None,
    stroke_width=1,
    size=None,
    interline=None,
    blur=None,
    method="label",
    tempfilename=None,
):
    """
    """

    tempfilename, error = txt_to_image(
        txt=txt,
        font=font,
        fontsize=fontsize,
        color=color,
        align=align,
        kerning=kerning,
        stroke_color=stroke_color,
        stroke_width=stroke_width,
        size=size,
        interline=interline,
        blur=blur,
        method=method,
        tempfilename=tempfilename,
        debug=True,
    )

    os.remove(tempfilename)
    lines = error.splitlines()[::-1]
    descent = None

    for line in lines:
        descent = re.search("descent: [-]?([0-9]*)", line)
        ascent = re.search("ascent: [-]?([0-9]*)", line)
        width = re.search("width: ([0-9]*)", line)
        height = re.search("height: ([0-9]*)", line)
        advance = re.search("advance: ([0-9]*)", line)
        # bounds = re.search("bounds: ([0-9.]*),", line)

        if descent is not None:
            break

    if descent is None:
        raise IOError(
            "This error can be due to the fact that ImageMagick is not find descent"
        )

    pointsize = re.search("pointsize ([0-9]*)", lines[0])

    metrics = {
        "width": int(width.group(1)),
        "height": int(height.group(1)),
        "ascent": int(ascent.group(1)),
        "descent": 0 - int(descent.group(1)),
        "advance": int(advance.group(1)),
        # "bounds": float(bounds.group(1)),
        "fontsize": int(pointsize.group(1)),
    }

    return metrics


def get_automatically_adjust_txt(
    txt,
    font,
    size,
    fontsize=None,
    color="black",
    align="center",
    kerning=None,
    stroke_color=None,
    method="caption",
    stroke_width=1,
    interline=None,
    blur=None,
):
    """
    """

    tempfilename, error = txt_to_image(
        txt=txt,
        font=font,
        fontsize=fontsize,
        color=color,
        align=align,
        kerning=kerning,
        stroke_color=stroke_color,
        stroke_width=stroke_width,
        size=size,
        interline=interline,
        blur=blur,
        method=method,
        debug=True
    )

    lines = error.splitlines()[::-1]
    pointsize = re.search("pointsize ([0-9]*)", lines[0])
    auto_lines_txt = []

    lines_txt = [
        re.search(r"Metrics: text: ([\s\S]+); width:", l).group(1)
        for l in lines
        if "Metrics: text:" in l and re.search(r"Metrics: text: ([\s\S]+); width:", l)
    ]

    clear_txt = txt.replace(" ", "").replace("\n", "")[::-1]
    new_clear_txt = ""

    for line in lines_txt:
        auto_lines_txt.append(line)

        new_clear_txt += line.replace(" ", "").replace("\n", "")[::-1]
        if txt.find(line) == 0 and new_clear_txt == clear_txt:
            break
        elif new_clear_txt == clear_txt:
            break

    auto_lines_txt = "\n".join(auto_lines_txt[::-1])
    count = 1000 # delete
    new_txt = ""
    merge_txt = ""

    for t in auto_lines_txt:
        tmp_txt = new_txt + t
        add_txt = ""

        while count > 0:
            if txt.find(tmp_txt) == 0:
                new_txt = tmp_txt
                merge_txt += add_txt + t
                break
            elif t == "\n":
                merge_txt += t
                tmp_txt = new_txt + " "

                if txt.find(tmp_txt) == 0:
                    new_txt = tmp_txt
                
                break

            add_txt += "\n"
            tmp_txt = new_txt + add_txt + t
            count -= 1

            if count <= 0:
                raise IOError("ImageMagick.txt concatenate fail!")

    size = Image.open(tempfilename).size
    os.remove(tempfilename)

    return merge_txt, int(pointsize.group(1)), size

TextLayoutClip.py

import os
import tempfile
import numpy as np
from PIL import Image
from tools import imagemagick_tools
from moviepy.editor import TextClip, ImageClip

class TextLayoutClip(TextClip):
    """"""

    def __init__(
        self,
        txt,
        font,
        fontsize=None,
        color="black",
        bg_color="transparent",
        bg_alpha=None,
        align="center",
        kerning=None,
        size=None,
        stroke_color=None,
        stroke_width=1,
        interline=None,
        blur=None,
        method="label",
        transparent=True,
    ):
        self.txt = txt
        self.font = font
        self.fontsize = 0 if fontsize is None else fontsize
        self.color = color
        self.bg_color = (
            "transparent" if bg_color is None or bg_color == "" else bg_color
        )
        self.bg_alpha = bg_alpha
        self.align = align
        self.kerning = kerning
        self.kerning_size = (
            (-1, -1)
            if size is None
            else tuple(map(lambda x: -1 if x is None or x == -1 else x, size))
        )
        self.stroke_color = stroke_color
        self.stroke_width = stroke_width
        self.interline = interline
        self.blur = blur
        self.method = method
        self.is_automatically_adjust_txt = False
        self._cs_letter_imgs = {}
        self._cs_tmp_file = []
        self._cs_line_letter_imgs = {}

        tempfile_fd, self.tempfilename = tempfile.mkstemp(suffix=".png")
        os.close(tempfile_fd)
        self.automatically_adjust_txt()

        # with hlp.timer("generater"):
        self.img = self._txt_to_image()
        self._layout_box()
        self._generate_image()

        ImageClip.__init__(self, np.array(self.img), transparent=transparent)

        os.remove(self.tempfilename)

    def position_x(self, bg_width, width, align="center"):
        if align.find("West") > -1:
            x = 0
        elif align.find("East") > -1:
            x = bg_width - width
        else:
            x = (bg_width - width) // 2

        return x

    def concat_horizontal(self, im1, im2, kerning=-1):
        dst = Image.new(
            "RGBA", (im1.width + im2.width + kerning, max(im1.height, im2.height))
        )
        dst.alpha_composite(im1, (0, 0))
        box = (im1.width + kerning, 0)
        dst.alpha_composite(im2, box)
        return dst, box

    def concat_vertical(self, im1, im2, interline=-1, align="center", total_width=0):
        total_width = max(im1.width, im2.width) if total_width == 0 else total_width
        dst = Image.new("RGBA", (total_width, im1.height + im2.height + interline))

        box = (self.position_x(bg_width=total_width, width=im1.width, align=align), 0)
        dst.alpha_composite(im1, box)
        box = (
            self.position_x(bg_width=total_width, width=im2.width, align=align),
            im1.height + interline,
        )
        dst.alpha_composite(im2, box)
        return dst, box


    def automatically_adjust_txt(self):
        adjust_attrs = {
            "txt": self.txt.replace("\\n", "\n"),
            "font": self.font,
            "fontsize": self.fontsize,
            "color": self.color,
            "kerning": self.kerning,
            "stroke_color": self.stroke_color,
            "stroke_width": self.stroke_width,
            "align": self.align,
            "size": self.kerning_size,
            "method": self.method,
        }

        if self.interline is not None:
            metrics = imagemagick_tools.get_font_metrics(**adjust_attrs)
            self.interline = self.interline + metrics["descent"]

        if (
            self.fontsize == 0 or self.fontsize is None or self.method == "caption"
        ):
            adjust_attrs["txt"] = self.txt.replace("\\n", "\n")
            adjust_attrs["interline"] = self.interline
            adjust_attrs["blur"] = self.blur
            adjust_attrs["fontsize"] = self.fontsize
            txt, fontsize, layout_size = imagemagick_tools.get_automatically_adjust_txt(
                **adjust_attrs
            )

            if self.method == "caption":
                self.txt = txt.replace("\n", "\\n")

            self.fontsize = fontsize
            self.kerning_size = layout_size
            self.is_automatically_adjust_txt = True

    def _txt_to_image(self):
        img = None
        # https://imagemagick.org/Usage/text/#font_info

        for line, txt in enumerate(self.txt.split("\\n")):
            line_im = None
            self._cs_letter_imgs.setdefault(line, [])
            line_letter_sync = ""
            txt = " " if txt == "" else txt

            for letter in txt:
                txt_to_image_attrs = {
                    "txt": letter,
                    "font": self.font,
                    "fontsize": self.fontsize,
                    "color": self.color,
                    "kerning": self.kerning,
                    "stroke_color": self.stroke_color,
                    "stroke_width": self.stroke_width,
                    "interline": self.interline,
                    "blur": self.blur,
                    "tempfilename": self.tempfilename,
                    "align": self.align,
                    "method": self.method,
                }

                imagemagick_tools.txt_to_image(**txt_to_image_attrs)
                im = Image.open(self.tempfilename)
                kerning = -1

                if line_letter_sync != "" and line_im is not None:
                    # TODO 性能优化,self.kerning - 1 的方式不一定准确。在少量实验中验证为正确。,无法应用在特殊字体上

                    tempfile_fd, tempfilename = tempfile.mkstemp(suffix=".png")
                    os.close(tempfile_fd)
                    line_letter_sync += letter
                    txt_to_image_attrs["txt"] = line_letter_sync
                    txt_to_image_attrs["tempfilename"] = tempfilename
                    line_letter_sync_img = imagemagick_tools.txt_to_image(
                        **txt_to_image_attrs
                    )
                    line_letter_sync_im = Image.open(line_letter_sync_img)
                    kerning = line_letter_sync_im.width - line_im.width - im.width
                    os.remove(tempfilename)
                    # kerning = int(self.kerning - 1)

                if line_im is None:
                    line_letter_sync = letter
                    line_im = im
                    box = (0, 0)
                else:
                    line_im, box = self.concat_horizontal(
                        im1=line_im, im2=im, kerning=kerning
                    )

                if letter != " ":
                    self._cs_letter_imgs[line].append(
                        {"data": np.array(im), "box": box, "size": im.size}
                    )

            if line_im is not None:
                self._cs_line_letter_imgs[line] = {"im": line_im, "box": (0, 0)}

        total_width = 0

        for line in self._cs_line_letter_imgs:
            total_width = max(total_width, self._cs_line_letter_imgs[line]["im"].width)

        for line in self._cs_line_letter_imgs:
            if img is None:
                img = self._cs_line_letter_imgs[line]["im"]
                box = (
                    self.position_x(
                        bg_width=total_width, width=img.width, align=self.align
                    ),
                    0,
                )
            else:
                img, box = self.concat_vertical(
                    im1=img,
                    im2=self._cs_line_letter_imgs[line]["im"],
                    align=self.align,
                    interline=-1 if self.interline is None else self.interline,
                    total_width=total_width,
                )

            self._cs_line_letter_imgs[line]["box"] = box

        return img

    def _layout_box(self):
        for line in self._cs_letter_imgs:
            line_x, line_y = self._cs_line_letter_imgs[line]["box"]
            for i, letter in enumerate(self._cs_letter_imgs[line]):
                x, y = self._cs_letter_imgs[line][i]["box"]
                self._cs_letter_imgs[line][i]["box"] = (line_x + x, line_y + y)

        layout_width = (
            self.kerning_size[0] if self.kerning_size[0] > 0 else self.img.size[0]
        )
        layout_height = (
            self.kerning_size[1] if self.kerning_size[1] > 0 else self.img.size[1]
        )
        self.kerning_size = (layout_width, layout_height)

        if self.kerning_size is None or self.kerning_size == self.img.size:
            self.kerning_size = self.img.size
            return

        if self.align.find("North") == 0:
            kerning_min_height = 0
            kerning_max_height = self.kerning_size[1]
        elif self.align.find("South") == 0:
            kerning_min_height = self.img.size[1] - self.kerning_size[1]
            kerning_max_height = (
                self.kerning_size[1]
                if kerning_min_height <= 0
                else kerning_min_height + self.kerning_size[1]
            )
        else:
            kerning_min_height = (self.img.size[1] - self.kerning_size[1]) // 2
            kerning_max_height = (
                self.kerning_size[1]
                if kerning_min_height <= 0
                else kerning_min_height + self.kerning_size[1]
            )

        if self.align.find("West") != -1:
            kerning_min_width = 0
            kerning_max_width = self.kerning_size[0]
        elif self.align.find("East") != -1:
            kerning_min_width = self.img.size[0] - self.kerning_size[0]
            kerning_max_width = (
                self.kerning_size[0]
                if kerning_min_width <= 0
                else kerning_min_width + self.kerning_size[0]
            )
        else:
            kerning_min_width = (self.img.size[0] - self.kerning_size[0]) // 2
            kerning_max_width = (
                self.kerning_size[0]
                if kerning_min_width <= 0
                else kerning_min_width + self.kerning_size[0]
            )

        new_letter_imgs = {}

        for line in self._cs_letter_imgs:
            new_letter_imgs.setdefault(line, [])

            for i, letter in enumerate(self._cs_letter_imgs[line]):
                origin_width, origin_height = letter["size"]
                origin_box_x, origin_box_y = letter["box"]
                is_crop = False

                if kerning_min_width < 0:
                    new_box_x = origin_box_x - kerning_min_width
                    x0, x1 = 0, origin_width
                else:
                    if (
                        origin_box_x >= kerning_max_width
                        or (origin_box_x + origin_width) <= kerning_min_width
                    ):
                        del self._cs_letter_imgs[line][i]
                        continue

                    if origin_box_x <= kerning_min_width:
                        x0, x1 = kerning_min_width - origin_box_x, origin_width
                        new_box_x = 0
                        is_crop = True
                    elif (origin_box_x + origin_width) > kerning_max_width:
                        x0, x1 = (
                            0,
                            origin_width
                            - (origin_box_x + origin_width - kerning_max_width),
                        )
                        new_box_x = origin_box_x - kerning_min_width
                        is_crop = True
                    else:
                        x0, x1 = 0, origin_width
                        new_box_x = origin_box_x - kerning_min_width

                if kerning_min_height < 0:
                    new_box_y = origin_box_y - kerning_min_height
                    y0, y1 = 0, origin_height
                else:
                    if (
                        origin_box_y >= kerning_max_height
                        or (origin_box_y + origin_height) <= kerning_min_height
                    ):
                        del self._cs_letter_imgs[line][i]
                        continue

                    if origin_box_y <= kerning_min_height:
                        y0, y1 = kerning_min_height - origin_box_y, origin_height
                        new_box_y = 0
                        is_crop = True
                    elif (origin_box_y + origin_height) > kerning_max_height:
                        y0, y1 = (
                            0,
                            origin_height
                            - (origin_box_y + origin_height - kerning_max_height),
                        )
                        new_box_y = origin_box_y - kerning_min_height
                        is_crop = True
                    else:
                        y0, y1 = 0, origin_height
                        new_box_y = origin_box_y - kerning_min_height

                letter["box"] = (new_box_x, new_box_y)

                if is_crop:
                    im = Image.fromarray(letter["data"])
                    im = im.crop((x0, y0, x1, y1))
                    letter["size"] = im.size
                    letter["data"] = np.array(im)

                new_letter_imgs[line].append(letter)

        self._cs_letter_imgs = new_letter_imgs

    def _generate_image(self):
        bg_color = (0, 0, 0, 0) if self.bg_color == "transparent" else self.bg_color
        img = Image.new(
            "RGBA",
            self.kerning_size,
            color=bg_color,
        )

        if self.bg_alpha is not None:
            img.putalpha(self.bg_alpha)

        self._cs_line_letter_imgs = {}

        for line in self._cs_letter_imgs:
            if len(self._cs_letter_imgs[line]) == 0:
                continue

            lh = max([_["size"][1] for _ in self._cs_letter_imgs[line]])
            line_img = Image.new(
                "RGBA",
                (self.kerning_size[0], lh),
                color=bg_color,
            )

            for i, letter in enumerate(self._cs_letter_imgs[line]):
                x, y = letter["box"]
                x, y = x, y
                self._cs_letter_imgs[line][i]["box"] = (x, y)
                im = Image.fromarray(letter["data"])
                img.alpha_composite(im, (x, y))
                line_img.alpha_composite(im, (x, 0))

            self._cs_line_letter_imgs[line] = {
                "data": np.array(line_img),
                "size": line_img.size,
                "box": (0, y),
            }

        self.img = img
        self.kerning_size = img.size

letterCollection.py

"""
这个模块针对字母类特效统一集中处理
"""


import numpy as np
import moviepy.editor as mpy
from editor.animation.fx.scale import scale


"""
过程动画集合

if animation in process_animations:
    duration_total = xx
    _method = 'animation' + animation
    for i, clip in enumerate(letters):
        clip = clip.set_duration(duration_total)
        attrs = {'clip':clip, 'i': i + 1, 'input_num': letter_count, 'duration': duration, 'last_show': last_show}
        letters[i] = eval(_method)(**attrs)

"""
process_animations = ['scale', 'fade']

def letterToImageClips(letters, direction='top,left', is_line=False, is_rand=False, position=(0, 0), alpha=255):
    """字母转换为ImageClip集合

    Parameters
    -----------------
    letters
        list   editor.TextLayoutClip.TextLayoutClip._cs_letter_imgs

    direction
        str 字母显示排序

    is_line
        bool 是否按行线方式显示

    is_rand
        bool 是否随机显示

    alpha
        int 透明度设置

    Returns
    -----------------
    list[ImageClip]
    """

    letter_clips = {} if is_line else []
    letters = adjustLetterSort(letters=letters, direction=direction)

    for line in letters:
        letter_imgs = letters[line] if isinstance(letters[line], list) else [letters[line]]
        line_clips = []
        
        for letter_img in letter_imgs:
            letter = mpy.ImageClip(letter_img['data'], transparent=True)

            if alpha != 255:
                letter = letter.set_opacity(alpha / 255)

            letter.screenpos = (letter_img['box'][0] + position[0], letter_img['box'][1] + position[1])
            line_clips.append(letter)
        
        if is_line:
            letter_clips[line] = line_clips
        else:
            letter_clips += line_clips

    if is_rand:
        l = len(letter_clips)
        rand_int = np.random.permutation(l)
        letter_clips = [letter_clips[_] for _ in rand_int]

    return letter_clips

def adjustLetterSort(letters, direction='top,left'):
    """调整字母排序

    Parameters
    ----------------------
    letters
        list
    direction
        str

    Returns
    ----------------------
    list
    """

    _from, _to = _direction(direction=direction)

    if _to == 'right':
        for _ in letters:
            letters[_] = letters[_][::-1]

    keys = sorted(list(letters.keys()))

    if _from == 'bottom':
        keys = keys[::-1]

    new_letters = {}

    for line in keys:
        new_letters[line] = letters[line]

    return new_letters


def _direction(direction='top,left'):
    """校正方向设置

    Parameters
    ------------------
    direction
        str

    Returns
    ------------------
    tuple
    """

    direction = direction.split(',')

    if len(direction) != 2:
        direction = ['top', 'left']

    _from = direction[0].strip().lower()
    _from = _from if _from in ['top', 'bottom'] else 'top'

    _to = direction[1].strip().lower()
    _to = _to if _to in ['left', 'right'] else 'left'
    return _from, _to


def moveLetters(letters, func, **kwargs):
    input_num = len(letters)
    final_clips = []

    if isinstance(letters, dict):
        for i, letter_index in enumerate(sorted(letters.keys())):
            kwargs['letter_layer_num'] = len(letters.get(letter_index, []))
            for l, letter in enumerate(letters.get(letter_index, [])):
                kwargs['letter_layer_index'] = l + 1
                final_clips.append(letter.set_position(func(letter.screenpos, i + 1, input_num, **kwargs)))
    else:
        for i, letter in enumerate(letters):
            final_clips.append(letter.set_position(func(letter.screenpos, i + 1, input_num, **kwargs)))

    return final_clips


def animation_slide(position, i, input_num, duration=1, begin=0, distance=0, angle=0):
    """ 滑动效果
    """
    v = np.array([np.cos(angle * np.pi / 180), np.sin(angle * np.pi / 180)])
    duration_total = duration
    duration = duration / input_num
    start_duration = duration * (i - 1)
    end_duration = duration * i
    d = lambda t: max(0, (end_duration - t) / duration) if start_duration <= t else 10000
    
    def fl(t):
        pos = position - distance * v * d(t)

        return pos

    return fl

example:

from TextLayoutClip import TextLayoutClip
from moviepy.editor import TextClip
import letterCollection

txt = "imagemagick"
font = "ARIAL.ttf"
clip = TextLayoutClip(txt=txt, fontsize=100, font=font, color='white')
clip = clip.set_duration(3).set_fps(30)
# print(clip._cs_letter_imgs)

# letters = letterCollection.letterToImageClips(
#         letters=clip._cs_letter_imgs)
# letters = sorted(letters, key=lambda c: c.screenpos[0])

clip.write_videofile("test.mp4")

clip = TextClip(txt=txt, fontsize=100, font=font, color='white')
clip = clip.set_duration(3).set_fps(30)

clip.write_videofile("orint_text_clip.mp4")
@clotyxf clotyxf added the question Questions regarding functionality, usage label Jun 29, 2020
@tburrows13
Copy link
Collaborator

Sorry, I only just saw this. Thanks so much for posting all of this. It looks like it could be really useful for adding more features to TextClip. As noted in #1145, I'd like to remove the need for ImageMagick in TextClips if possible, but I think that a lot of the code here would still work with a PIL-based TextClip, so I'll look at integrating it into my branch at some point.

@clotyxf clotyxf closed this as completed Jul 9, 2020
@derekcbr
Copy link

from editor.animation.fx.scale import scale shows ModuleNotFoundError: No module named 'editor'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Questions regarding functionality, usage
Projects
None yet
Development

No branches or pull requests

3 participants