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

Add gif support #3

Open
wants to merge 2 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
241 changes: 166 additions & 75 deletions src/backend.py

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion src/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import os

from lark import Lark
from PIL import Image, PngImagePlugin
from PIL import Image, PngImagePlugin, GifImagePlugin

from .transform_parse_tree import ConvertParseTree
from .stack_manager import StackManager
Expand All @@ -13,6 +13,7 @@


def main():
GifImagePlugin.LOADING_STRATEGY = GifImagePlugin.LoadingStrategy.RGB_ALWAYS
dirname = os.path.dirname(__file__)
filename = os.path.join(dirname, 'grammar_enforcing.lark')

Expand Down Expand Up @@ -103,6 +104,14 @@ def memestr_to_img(memestr: str):
elif args.preview:
img.show()

elif img.is_animated:
filename = os.path.join(os.getcwd(), args.outputfile)
if filename.endswith('.png'):
filename = filename.removesuffix('.png') + '.gif'
info = img.info
info['comment'] = f'memesource: {memestr}'
img.save(filename, save_all=True)

else:
filename = os.path.join(os.getcwd(), args.outputfile)
info = PngImagePlugin.PngInfo()
Expand Down
42 changes: 23 additions & 19 deletions src/defines.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,37 @@

import os.path
from enum import Enum, auto, unique

# We were worried about too much in utils, so we now have a separation.
# This file is meant for all the various global defaults and settings.

DEFAULT_SIZE = (640, 480)
DEFAULT_FONT_SIZE = 100

OFFSET_WHITESPACE = 5
OFFSET_WHITESPACE = 5

FML_DIR = os.path.join(os.path.expanduser("~"), '.local', 'lib', 'meme', 'fml')
LIB_DIR = os.path.join(os.path.expanduser("~"), '.local', 'lib', 'meme', 'libs')
LIB_DIR = os.path.join(os.path.expanduser(
"~"), '.local', 'lib', 'meme', 'libs')
FML_URL = "https://github.com/schorrm/fml.git"

DEFAULT_FIELD_CFG = { "RIGHT" : ("50%", "0%", "100%", "100%"), # r1..rn
"LEFT" : ( "0%", "0%", "50%", "100%"), # l1..ln
"top" : ( "0%", "0%", "100%", "20%"),
"bottom" : ( "0%", "80%", "100%", "100%"),
"center" : ( "0%", "40%", "100%", "60%"),
"rtop" : ("50%", "0%", "100%", "20%"),
"rbottom": ("50%", "80%", "100%", "100%"),
"rcenter": ("50%", "40%", "100%", "60%"),
"ltop" : ( "0%", "0%", "50%", "20%"),
"lbottom": ( "0%", "80%", "50%", "100%"),
"lcenter": ( "0%", "40%", "50%", "60%")
}
DEFAULT_FIELD_ORDER = ("top", "bottom", "center", "rtop", "rbottom", "rcenter", "ltop", "lbottom", "lcenter")

CONFIG_EXT = '.memeconfig' # Would still be neat to support embedding this stuff in JPEG EXIF fields / PNG Text chunks. -> later version
DEFAULT_FIELD_CFG = {"RIGHT": ("50%", "0%", "100%", "100%"), # r1..rn
"LEFT": ("0%", "0%", "50%", "100%"), # l1..ln
"top": ("0%", "0%", "100%", "20%"),
"bottom": ("0%", "80%", "100%", "100%"),
"center": ("0%", "40%", "100%", "60%"),
"rtop": ("50%", "0%", "100%", "20%"),
"rbottom": ("50%", "80%", "100%", "100%"),
"rcenter": ("50%", "40%", "100%", "60%"),
"ltop": ("0%", "0%", "50%", "20%"),
"lbottom": ("0%", "80%", "50%", "100%"),
"lcenter": ("0%", "40%", "50%", "60%")
}
DEFAULT_FIELD_ORDER = ("top", "bottom", "center", "rtop",
"rbottom", "rcenter", "ltop", "lbottom", "lcenter")

# Would still be neat to support embedding this stuff in JPEG EXIF fields / PNG Text chunks. -> later version
CONFIG_EXT = '.memeconfig'

# Default formatting for WP to meme
WP_DEFAULT_FONT = 'arial'
Expand All @@ -40,6 +43,7 @@
ALIGN_DATA = ['halign', 'valign']
COLOR_DATA = ['foreground', 'background', 'outline']


@unique
class TagType(Enum):
FONT = auto()
Expand All @@ -51,7 +55,7 @@ class TagType(Enum):
MEME = auto()
COMPOSITE = auto()
WHITESPACE = auto()

TIME = auto()

@property
def is_format(self):
Expand Down
18 changes: 16 additions & 2 deletions src/format.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .format_types import Font, Alignment, Color
from .format_types import Font, Alignment, Color, Time
import warnings
from .defines import TagType

Expand All @@ -10,10 +10,11 @@

class FormatManager:
class FormatContext:
def __init__(self, def_font=Font(), def_align=Alignment(), def_color=Color()):
def __init__(self, def_font=Font(), def_align=Alignment(), def_color=Color(), def_time=Time()):
self.fonts = [def_font]
self.aligns = [def_align]
self.colors = [def_color]
self.times = [def_time]

@property
def current_font(self):
Expand All @@ -31,6 +32,10 @@ def current_color(self):
def current_format(self):
return (self.current_font, self.current_align, self.current_color)

@property
def current_time(self) -> Time:
return self.times[-1]

def F_tag(self, font):
self.fonts.append(font.inherit_from(self.current_font))

Expand All @@ -56,6 +61,8 @@ def update_context(self, tag):
warnings.warn(
"Got POP tag in update_context(). pop_tag() should be called explicitly", SyntaxWarning)
self.pop_tag(tag)
elif tag.type == TagType.TIME:
self.times.append(tag)
else:
# TODO: Usefuller error messages. Own error type(s)?
raise RuntimeError("Got bad tag type")
Expand All @@ -69,6 +76,8 @@ def pop_tag(self, tag):
target_array = self.aligns
case TagType.COLOR:
target_array = self.colors
case TagType.TIME:
target_array = self.times
case _:
raise RuntimeError("Invalid pop tag data")

Expand All @@ -81,6 +90,7 @@ def _flatten(self):
self.fonts = [self.current_font]
self.aligns = [self.current_align]
self.colors = [self.current_color]
self.times = [self.current_time]

@property
def _current_context(self):
Expand All @@ -89,6 +99,10 @@ def _current_context(self):
def __init__(self):
self.contexts = [FormatManager.FormatContext()]

def contains_frame(self, frame_number: int) -> bool:
start_frame, end_frame = self._current_context.current_time.frames
return start_frame <= frame_number <= end_frame

def push_context(self, scoped_tags):
""" Adds a new context (Container or Meme) to the stack """
self.contexts.append(FormatManager.FormatContext(self.current_font,
Expand Down
9 changes: 8 additions & 1 deletion src/format_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def _STATIC(tag: TagType) -> TagType:
class Font:
font_face: str = 'Impact'
font_size: int = DEFAULT_FONT_SIZE
outline_size: int = 0
outline_size: int = 2
text_style: str = 'r'
type: TagType = _STATIC(TagType.FONT)
_cached_font: FreeTypeFont | None = field(init=False, default=None)
Expand Down Expand Up @@ -75,6 +75,13 @@ def __repr__(self):
return f'<Color: FG={self.foreground}, BG={self.background}, OL={self.outline}>'


@dataclass
class Time:
frames: tuple[int | None, int | None] | None = None
seconds: tuple[float | None, float | None] | None = None
type: TagType = _STATIC(TagType.TIME)


# TODO: Support text style -- we need to figure a lot of other details here, may need tweaks
# Not guaranteed support in 1.0
class TextStyle:
Expand Down
12 changes: 12 additions & 0 deletions src/grammar_enforcing.lark
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ _format_block: font
| textstyle
| colorblock
| align
| time

colorblock: "CL" (colorarg) ~ 0..3
colorarg: ":" COLOR?
Expand All @@ -48,6 +49,14 @@ valign: ":" HEIGHTAL
COLUMN: "right" | "center" | "left" | "r" | "c" | "l"
HEIGHTAL: "top" | "center" | "bottom" | "t" | "c" | "b" | "rtop" | "rbottom" | "rcenter" | "ltop" | "lbottom"| "lcenter"

time: "TIME" ":" (secondsdirective | framesdirective)
secondsdirective: range
framesdirective: "f" range
range: closed | less_than | at_least
closed: DEC_OR_INT "-" DEC_OR_INT
less_than: "-" DEC_OR_INT
at_least: DEC_OR_INT "-"

font: "F" (fontname (":" FONTSIZE?)? (":" OUTLINESIZE)?)?

fontname: ":" CNAME?
Expand All @@ -73,10 +82,13 @@ _ESCAPE_SAFE: /(.+?)(?<!~)(~~)*?(?=[\/:;])/ //
VALID_BLOCK: _TILDE_SOLVE | _ESCAPE_SAFE // Order here is crucial for some reason
// _TILDE_SOLVE: /(~~)+?(?=[\/:])/

DEC_OR_INT: INT | DECIMAL

%import common.CNAME
%import common.LETTER
%import common.DIGIT
%import common.HEXDIGIT
%import common.DECIMAL
%import common.SIGNED_NUMBER
%import common.INT
%import common.ESCAPED_STRING
3 changes: 2 additions & 1 deletion src/layout_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Class for intermediate layout objects
'''

from .utils import get_image_size
from .utils import get_image_size, BBox
from .defines import DEFAULT_SIZE, TagType
from typing import Any, Dict
from dataclasses import dataclass
Expand Down Expand Up @@ -44,6 +44,7 @@ class LPText(LPTag):
position: str | None = None
rotation: int = 0
type: TagType = TagType.TEXT
resolved_position: None | BBox = None

def __repr__(self):
return f'<Text "{self.text}" @{self.position}>'
Expand Down
4 changes: 3 additions & 1 deletion src/stack_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,10 @@ def DrawStack(self, scope: Scope) -> Image:
else:
image = self.drawing_manager.DrawMeme(
child.tag, child.scoped_tags, child.children)
if image.is_animated:
return image # only one animated gif
images.append(image)
elif child.type.is_format:
elif child.type.is_format or child.type == TagType.TIME:
self.drawing_manager.format_manager.update_context(child)

elif child.type == TagType.POP and child.target.is_format:
Expand Down
32 changes: 29 additions & 3 deletions src/transform_parse_tree.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
#!/usr/bin/python3

import copy
from lark import Lark, Transformer

from .utils import unpack, unpack3, list2dict
from .defines import TagType

from .format_types import Font, Alignment, Color
from .format_types import Font, Alignment, Color, Time
from .layout_objects import LPMeme, LPText, LPComposite, LPWhitespacePrefix, Pop

ESCAPE_CHAR = '~'
Expand All @@ -15,7 +16,6 @@
't': '\t'
}

import copy

def _extract_monic(tree):
if tree:
Expand Down Expand Up @@ -100,7 +100,7 @@ def text(self, token):
processed_text = ''
escape = False
for char in text:
if escape: # previous char was escaped
if escape: # previous char was escaped
processed_text += escape_map.get(char, char)
escape = False
elif char == ESCAPE_CHAR:
Expand Down Expand Up @@ -134,3 +134,29 @@ def textblock(self, tree):

def whitespaceprefix(self, text):
return LPWhitespacePrefix(**text[0])

def time(self, tree):
tree = list2dict(tree)
return Time(**tree)

def secondsdirective(self, tree):
return {'seconds': tree[0]}

def framesdirective(self, bounds):
bounds = [int(b) if b is not None else None for b in bounds[0]]
return {'frames': bounds}

def range(self, tree):
return tree[0]

def closed(self, bounds):
upper, lower = bounds
return (float(upper), float(lower))

def at_least(self, min):
min = _extract_monic(min)
return (float(min), None)

def less_than(self, max):
max = _extract_monic(max)
return (None, float(max))