Skip to content

Commit

Permalink
add support for gifs
Browse files Browse the repository at this point in the history
  • Loading branch information
schorrm committed Apr 7, 2023
1 parent 43e822d commit 4619308
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 103 deletions.
238 changes: 163 additions & 75 deletions src/backend.py

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion src/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,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 @@ -14,6 +14,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 @@ -104,6 +105,12 @@ def memestr_to_img(memestr: str):
elif args.preview:
img.show()

elif img.is_animated:
filename = os.path.join(os.getcwd(), args.outputfile)
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 @@ -129,8 +129,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))

0 comments on commit 4619308

Please sign in to comment.