diff --git a/src/backend.py b/src/backend.py index 3aad431..23cfcb1 100644 --- a/src/backend.py +++ b/src/backend.py @@ -10,14 +10,17 @@ from contextlib import suppress import warnings import json +from io import BytesIO + from bidi.algorithm import get_display from .layout_objects import LPComposite, LPMeme, LPText, LPWhitespacePrefix -from .format_types import Font, Color, Alignment +from .format_types import Font, Color, Alignment, Time from PIL import Image, ImageDraw + class Meme: _default_config = DEFAULT_FIELD_CFG _default_order = DEFAULT_FIELD_ORDER @@ -30,24 +33,31 @@ def __init__(self, image_handle: str, size: Coordinates, fillcolor: str = 'white else: file_path = resolve_file_path(image_handle) image = Image.open(file_path) - image = image.convert('RGBA') - if size[0] and size[1] and image.size != size: - - if mode == "resize": - if image.size[0]/size[0] != image.size[1]/size[1]: - warnings.warn(f"Resizing image from {image.size} to {size} doesn't preserve aspect ratio", RuntimeWarning) - image = image.resize(size) - elif mode == "crop": - image = image.crop(get_bbox(smaller=size, larger=image)) - elif mode == "fill": - image = Image.new('RGBA', size, fillcolor).paste(image, get_bbox(smaller=image, larger=size)) - else: - raise RuntimeError("Bad Mode") + self.is_gif = True + if not file_path.endswith('.gif'): + self.is_gif = False + image = image.convert('RGBA') + if size[0] and size[1] and image.size != size: + + if mode == "resize": + if image.size[0]/size[0] != image.size[1]/size[1]: + warnings.warn( + f"Resizing image from {image.size} to {size} doesn't preserve aspect ratio", RuntimeWarning) + image = image.resize(size) + elif mode == "crop": + image = image.crop( + get_bbox(smaller=size, larger=image)) + elif mode == "fill": + image = Image.new('RGBA', size, fillcolor).paste( + image, get_bbox(smaller=image, larger=size)) + else: + raise RuntimeError("Bad Mode") self.image = image self.load_config(file_path) self.max_row = 1 - self.draw = ImageDraw.Draw(self.image, mode="RGBA") + self.deferred_texts_and_times = [] + # self.draw = ImageDraw.Draw(self.image, mode=self.draw_mode) @property def width(self): @@ -60,23 +70,24 @@ def height(self): def _convert_percentage_values(self, coords: BBox) -> BBox: axes = [self.width, self.height, self.width, self.height] coords = list(coords) - for i, (direction, max_value) in enumerate(zip(coords, axes)): # l t r b - if type(direction) == str and direction.endswith("%"): # TODO: NOTE: This may not come as percent, + for i, (direction, max_value) in enumerate(zip(coords, axes)): # l t r b + # TODO: NOTE: This may not come as percent, + if type(direction) == str and direction.endswith("%"): # we need to watch this, given that % is reserved - coords[i] = round((int(direction[:-1]) / 100) * max_value) # convert all % to pixel values + # convert all % to pixel values + coords[i] = round((int(direction[:-1]) / 100) * max_value) elif type(direction) == str: coords[i] = int(direction) return tuple(coords) - def load_config(self, file_path: str): self.fields = Meme._default_config.copy() config_path = file_path + CONFIG_EXT self.field_order = Meme._default_order self.default_format = [] if os.path.exists(config_path): - with open (config_path) as f: + with open(config_path) as f: field_data = json.load(f) self.fields.update(field_data.get("namedFields", {})) if field_data.get("defaultOrder"): @@ -84,16 +95,17 @@ def load_config(self, file_path: str): format_data = field_data.get("format_data") if format_data is not None: def _make_dict(name, args): - d = {arg : None for arg in args} + d = {arg: None for arg in args} d.update(format_data.get(name, {})) return d self.default_format = [Font(**_make_dict("font", FONT_DATA)), - Alignment(**_make_dict("alignment", ALIGN_DATA)), + Alignment( + **_make_dict("alignment", ALIGN_DATA)), Color(**_make_dict("color", COLOR_DATA))] - self.fields["all"] = ("0%", "0%", "100%", "100%") # this is defined here so that its a super untouchable reserved thingy + # this is defined here so that its a super untouchable reserved thingy + self.fields["all"] = ("0%", "0%", "100%", "100%") - self.active_mode = 'general' self.mode_generators = { # self.fields only has str indices, so if self.field_order[idx] is a tuple we get None and return the tuple @@ -106,7 +118,7 @@ def _make_dict(name, args): "r": 1, "l": 1 } - + @property def active_index(self): return self.mode_indices[self.active_mode] @@ -128,7 +140,7 @@ def update_max_row(self, tag: LPText): if position == None: # Update always done by fallthrough, do_update just stops general->(r|l)n derailing if self.active_mode == "general": - position = self.active_index # fallthrough to int case + position = self.active_index # fallthrough to int case else: position = f'{self.active_mode}{self.active_index}' @@ -136,7 +148,8 @@ def update_max_row(self, tag: LPText): if type(position) == int: self.active_mode = "general" self.update_index(position) - position = self.field_order[self.active_index] # fallthrough if general order contains (r|l)n directives + # fallthrough if general order contains (r|l)n directives + position = self.field_order[self.active_index] do_update = False # (r|l)n @@ -144,60 +157,64 @@ def update_max_row(self, tag: LPText): if position[0] in "rl" and position[1].isdigit(): row = int(position[1:]) self.max_row = max(self.max_row, row) - if do_update: # only override the current stuff if this isn't a fallthrough from the general ordering + if do_update: # only override the current stuff if this isn't a fallthrough from the general ordering self.active_mode = position[0] self.update_index(row) - self.update_index() # advance to next position - + self.update_index() # advance to next position + def build_lookup_table(self): # make fields safe for k, v in self.fields.items(): self.fields[k] = self._convert_percentage_values(v) - + rleft, rtop, rright, rbottom = self.fields["RIGHT"] lleft, ltop, lright, lbottom = self.fields["LEFT"] - rdelta = (rbottom - rtop) / self.max_row # this may need to be // - ldelta = (lbottom - ltop) / self.max_row # this may need to be // + rdelta = (rbottom - rtop) / self.max_row # this may need to be // + ldelta = (lbottom - ltop) / self.max_row # this may need to be // rbaseline = rtop lbaseline = ltop for i in range(1, self.max_row + 1): - self.fields[f'r{i}'] = (rleft, round(rbaseline), rright, round(rbaseline + rdelta)) - self.fields[f'l{i}'] = (lleft, round(lbaseline), lright, round(lbaseline + ldelta)) + self.fields[f'r{i}'] = (rleft, round( + rbaseline), rright, round(rbaseline + rdelta)) + self.fields[f'l{i}'] = (lleft, round( + lbaseline), lright, round(lbaseline + ldelta)) rbaseline += rdelta lbaseline += ldelta # Reset mode_indices for wet run - self.mode_indices = {"general": 0, "r": 1, "l": 1 } + self.mode_indices = {"general": 0, "r": 1, "l": 1} - def resolve_position(self, position: Union[str, BBox, int, None]): - if type(position) == int: # general position index -> tuple/named position + def resolve_position(self, position: Union[str, BBox, int, None]) -> BBox: + if type(position) == int: # general position index -> tuple/named position self.active_mode = "general" self.update_index(position) position = self.next_position - elif type(position) == str: # this should be a named position, find appropriate tuple + elif type(position) == str: # this should be a named position, find appropriate tuple # allow auto position to work after l/r explicit position - if position[0] in "lr" and position[1].isdigit(): # NOTE: lexer-parser MUST use "left" and "right" + # NOTE: lexer-parser MUST use "left" and "right" + if position[0] in "lr" and position[1].isdigit(): self.active_mode = position[0] self.update_index(int(position[1:])) - elif position in self.field_order: # allow implicit following of general order + elif position in self.field_order: # allow implicit following of general order self.active_mode = "general" self.update_index(self.field_order.index(position)) try: # Not using next_position to explicitly catch bad names position = self.fields[position] except: - raise KeyError("Me looking for your named position directive like") - elif not position: # auto case + raise KeyError( + "Me looking for your named position directive like") + elif not position: # auto case position = self.next_position - + # self.next_position is guaranteed to be the current position at this point # so here we prep for the next call self.update_index() - + # handle percentages return self._convert_percentage_values(position) @@ -205,20 +222,44 @@ def add_text(self, text_img: Image, position: Coordinates): ''' Draw text to a location ''' self.image.alpha_composite(text_img, position) + def add_text_to_gif(self, text_img: Image, position: Coordinates, frames: tuple[int, int]): + self.deferred_texts_and_times.append([text_img, position, frames]) + + def generate_gif(self) -> list[Image]: + assert self.is_gif + + frames = [] + + for frame_num in range(self.image.n_frames): + self.image.seek(frame_num) + new_frame = Image.new('RGBA', self.image.size) + new_frame.paste(self.image) + for text, position, (minframe, maxframe) in self.deferred_texts_and_times: + if minframe <= frame_num <= maxframe: + new_frame.alpha_composite(text, position) + frames.append(new_frame) + + return frames + + class DrawingManager: def __init__(self): self.format_manager = FormatManager() + self.frame: int = 0 + self.drawing_mode: str = 'RGBA' + self.is_gif: bool = False def DrawTextMeme(self, tag: LPMeme, scoped_tags=[], child_tags=[]) -> Image: self.format_manager.push_context(scoped_tags) - + text_tags = [tag for tag in child_tags if tag.type == TagType.TEXT] if len(text_tags) != 1: raise SyntaxError("Undefined Text Thingy") text = text_tags[0] context = self.format_manager.scoped_context(text.scoped_tags) - _,_, (_, new_height) = optimize_text(text.tag.text, context.current_font, tag.size[0]) # no max height, as high as we need - + _, _, (_, new_height) = optimize_text(text.tag.text, + context.current_font, tag.size[0]) # no max height, as high as we need + tag.size = (tag.size[0], new_height + OFFSET_WHITESPACE) meme = Meme(tag.image, tag.size, tag.fillcolor, tag.mode) @@ -227,60 +268,107 @@ def DrawTextMeme(self, tag: LPMeme, scoped_tags=[], child_tags=[]) -> Image: # for tag_or_scope in child_tags: # if tag_or_scope.type == TagType.TEXT: # else: - # raise SyntaxError("How did you even get a format tag here?") + # raise SyntaxError("How did you even get a format tag here?") self.format_manager.pop_context() return meme.image - def DrawMeme(self, tag: LPMeme, scoped_tags=[], child_tags=[]) -> Image: - - ''' Draw a meme ''' meme = Meme(tag.image, tag.size, tag.fillcolor, tag.mode) self.format_manager.push_context(meme.default_format + scoped_tags) - + for tag in child_tags: if tag.type == TagType.TEXT: meme.update_max_row(tag.tag) meme.build_lookup_table() - for tag_or_scope in child_tags: - if tag_or_scope.type == TagType.TEXT: - self.DrawText(meme, tag_or_scope) + if meme.is_gif and meme.image.is_animated: + self.is_gif = True + # self.drawing_mode = 'RGB' + total_time = meme.image.n_frames * meme.image.info['duration'] + last_frame = meme.image.n_frames - 1 + + def seconds_to_frame(seconds: float) -> int: + seconds = min(seconds, total_time) + return round((seconds / total_time) * last_frame) + + def process_time_directive(time: Time) -> Time: + if time.seconds: + start, end = time.seconds + start_frame = seconds_to_frame(start) if start else 0 + end_frame = seconds_to_frame(end) if end else last_frame + return Time(frames=(start_frame, end_frame)) - elif tag_or_scope.type == TagType.POP: - self.format_manager.pop_tag(tag_or_scope.target) + else: + start, end = time.frames + start_frame = start or 0 + end_frame = end or 0 + return Time(frames=(start_frame, end_frame)) + + for tag_or_scope in child_tags: + match tag_or_scope.type: + case TagType.TIME: + self.format_manager.update_context( + process_time_directive(tag_or_scope)) + case TagType.POP: + self.format_manager.pop_tag(tag_or_scope.target) + case TagType.TEXT: + self.DrawText(meme, tag_or_scope) + case _: + self.format_manager.update_context(tag_or_scope) + + frames = meme.generate_gif() + base = frames[0] + base.info = meme.image.info + stream = BytesIO() + base.save(stream, format='gif', save_all=True, + append_images=frames[1:]) + im = Image.open(stream) + return im + + else: + for tag_or_scope in child_tags: + if tag_or_scope.type == TagType.TEXT: + self.DrawText(meme, tag_or_scope) - else: # otherwise it's a formatting tag - self.format_manager.update_context(tag_or_scope) + elif tag_or_scope.type == TagType.POP: + self.format_manager.pop_tag(tag_or_scope.target) + + else: # otherwise it's a formatting tag + self.format_manager.update_context(tag_or_scope) self.format_manager.pop_context() return meme.image def DrawText(self, meme, scope): - text = scope.tag.text + text = scope.tag.text position = scope.tag.position - #rotation = scope.tag.rotation + scope.tag.resolved_position = scope.tag.resolved_position or meme.resolve_position( + position) + # rotation = scope.tag.rotation + bbox = scope.tag.resolved_position - bbox = meme.resolve_position(position) - - width = bbox[2] - bbox[0] # r - l - height = bbox[3] - bbox[1] # if meme.has_height else None # b - t + width = bbox[2] - bbox[0] # r - l + height = bbox[3] - bbox[1] # if meme.has_height else None # b - t - context = self.format_manager.scoped_context(scope.scoped_tags) # how does this work? don't care, resolve scoped tags with current context + # how does this work? don't care, resolve scoped tags with current context + context = self.format_manager.scoped_context(scope.scoped_tags) - final_text, final_font, (final_width, final_height) = optimize_text(text, context.current_font, width, height) + final_text, final_font, (final_width, final_height) = optimize_text( + text, context.current_font, width, height) final_text = get_display(final_text) if height is None: height = final_height if context.current_color.background: - temp = Image.new("RGBA", (width, height), color=context.current_color.background) + temp = Image.new(self.drawing_mode, (width, height), + color=context.current_color.background) else: - temp = Image.new("RGBA", (width, height), color=(0,0,0,0)) + temp = Image.new(self.drawing_mode, + (width, height), color=(0, 0, 0, 0)) valign = context.current_align.valign or "top" halign = context.current_align.halign or "center" @@ -298,10 +386,10 @@ def DrawText(self, meme, scope): else: x = (width - final_width)/2 - draw = ImageDraw.Draw(temp, mode='RGBA') + draw = ImageDraw.Draw(temp, mode=self.drawing_mode) draw.text((x, y), final_text, fill=context.current_color.foreground, font=final_font, - align=halign, stroke_width=context.current_font.outline_size, - stroke_fill=context.current_color.outline) + align=halign, stroke_width=context.current_font.outline_size, + stroke_fill=context.current_color.outline) # if rotation != 0: # rotation is a fucking mess, v2.0 # temp = temp.rotate(rotation, expand=True) # new_width, new_height = temp.getsize() @@ -309,5 +397,8 @@ def DrawText(self, meme, scope): # y = bbox[1] + (height - new_height)/2 # else: # x, y, _, _ = bbox - meme.add_text(temp, bbox[:2]) # paste to actual image - + if self.is_gif: + meme.add_text_to_gif( + temp, bbox[:2], self.format_manager._current_context.current_time.frames) + else: + meme.add_text(temp, bbox[:2]) # paste to actual image diff --git a/src/console.py b/src/console.py index 16ff4b9..ca7581e 100644 --- a/src/console.py +++ b/src/console.py @@ -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 @@ -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') @@ -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() diff --git a/src/defines.py b/src/defines.py index 2195212..7120cf3 100644 --- a/src/defines.py +++ b/src/defines.py @@ -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' @@ -40,6 +43,7 @@ ALIGN_DATA = ['halign', 'valign'] COLOR_DATA = ['foreground', 'background', 'outline'] + @unique class TagType(Enum): FONT = auto() @@ -51,7 +55,7 @@ class TagType(Enum): MEME = auto() COMPOSITE = auto() WHITESPACE = auto() - + TIME = auto() @property def is_format(self): diff --git a/src/format.py b/src/format.py index 12dcbf0..7ab28f4 100644 --- a/src/format.py +++ b/src/format.py @@ -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 @@ -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): @@ -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)) @@ -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") @@ -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") @@ -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): @@ -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, diff --git a/src/format_types.py b/src/format_types.py index 9874879..99431db 100644 --- a/src/format_types.py +++ b/src/format_types.py @@ -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) @@ -75,6 +75,13 @@ def __repr__(self): return f'' +@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: diff --git a/src/grammar_enforcing.lark b/src/grammar_enforcing.lark index 6b97c3c..1a08bcc 100644 --- a/src/grammar_enforcing.lark +++ b/src/grammar_enforcing.lark @@ -36,6 +36,7 @@ _format_block: font | textstyle | colorblock | align + | time colorblock: "CL" (colorarg) ~ 0..3 colorarg: ":" COLOR? @@ -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? @@ -73,10 +82,13 @@ _ESCAPE_SAFE: /(.+?)(?' diff --git a/src/stack_manager.py b/src/stack_manager.py index 83469fa..6ad50ad 100644 --- a/src/stack_manager.py +++ b/src/stack_manager.py @@ -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: diff --git a/src/transform_parse_tree.py b/src/transform_parse_tree.py index 4c86d1a..27f9358 100644 --- a/src/transform_parse_tree.py +++ b/src/transform_parse_tree.py @@ -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 = '~' @@ -15,7 +16,6 @@ 't': '\t' } -import copy def _extract_monic(tree): if tree: @@ -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: @@ -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))