Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/python3
- """
- generate_img.py version 0.0.99.20211018 (2021-10-18) written by cleemy desu wayo
- this is a personal work and is a PDS (Public Domain Software)
- --------
- requirements: Python 3.8 or later, PIL (Pillow), GNU Unifont
- (and maybe this code works under only Linux)
- on Ubuntu, GNU Unifont becomes available by doing "sudo apt-get install unifont"
- --------
- see https://twitter.com/cleemy4545/status/1450188684045787142 and the thread
- old version: https://pastebin.com/JJPXFtfW
- """
- import os
- import sys
- import re
- from unicodedata import category
- from PIL import Image, ImageDraw, ImageFont
- class GenerateImg:
- """
- main class of generate_img.py
- """
- @property
- def __version__(self):
- return '0.0.99.xx'
- def __init__(self):
- self.__DEFAULT_FONT = 'unifont'
- self.__file_prefix_allowlist = ('./', '/')
- self.result_img = None
- self.dir_img = './'
- self.dir_textimg = './'
- self.is_dir_textimg_overridden = False
- self.pos = {'x': 0, 'y':0}
- self.margin = {'x': 0, 'y':0}
- self.step = {'x': 1, 'y':0}
- self.text_size = 16
- self.text_color = (0,0,0)
- self.text_font = self.__DEFAULT_FONT
- self.text_step = 1
- def __len__(self):
- return self.width
- @property
- def img(self):
- """
- return main image object
- """
- return self.result_img
- @property
- def width(self):
- """
- return width of main image object
- (return 0 if image is None)
- """
- if self.result_img is None:
- return 0
- return self.result_img.width
- @property
- def height(self):
- """
- return height of main image object
- (return 0 if image is None)
- """
- if self.result_img is None:
- return 0
- return self.result_img.height
- @property
- def file_prefix_allowlist(self):
- """
- return an allowlist of file prefix
- """
- return self.__file_prefix_allowlist
- def paste_img(self, params):
- """
- draw new image and return a tuple (loaded image size)
- """
- img_piece = Image.open(params['src']).convert('RGBA')
- img_tmp_canvas = Image.new('RGBA', self.result_img.size, (255, 255, 255, 0))
- img_tmp_canvas.paste(img_piece, (round(params['x']), round(params['y'])))
- self.result_img = Image.alpha_composite(self.result_img, img_tmp_canvas)
- return img_piece.size
- @classmethod
- def parse_args(cls, source_str):
- """
- parse source_str and return list
- '(100,100,20,#ffffff)' --> return ['100', '100', '20', '#ffffff']
- '(100)' --> return ['100']
- ' (100) ' --> return ['100']
- '( 100 , 100 )' --> return ['100', '100']
- '( 100 , 10 0 )' --> return ['100', '10 0']
- '(,,20)' --> return ['', '', '20']
- '(20,,)' --> return ['20', '', '']
- '(,,,)' --> return ['', '', '', '']
- '( , , , )' --> return ['', '', '', '']
- '()' --> return ['']
- '( )' --> return ['']
- '' --> return []
- '(())' --> return ['()']
- '(100,100' (invalid string) --> return []
- [100, 100] (not string) --> return []
- """
- if not isinstance(source_str, str):
- return []
- args_str = re.sub(r'^\((.*)\)$', '\\g<1>', source_str.strip()) # ' (100,100) ' --> '100,100'
- if args_str == source_str:
- return []
- return [s.strip() for s in args_str.split(',')]
- def proper_file_path(self, file_path, base_file_path_src = ''):
- """
- return a proper file path
- this ignores base_file_path_src if file_path starts with '/', 'file:///',
- 'https://', 'ftp://' or some such
- """
- # has a control code?
- if any(s for s in f'{file_path}{base_file_path_src}' if category(s) == 'Cc'):
- return ''
- result_str = ''
- if file_path.startswith('/') or file_path.startswith('file:///'):
- result_str = re.sub(r'^(file:)?\/+', '/', file_path)
- elif re.match(r'^[a-zA-Z]+:\/\/', file_path):
- result_str = re.sub(r'^([a-zA-Z]+):\/\/+', '\\g<1>://', file_path)
- else:
- base_file_path = f'{base_file_path_src}'
- if (base_file_path_src is None) or (base_file_path == ''):
- base_file_path = '.'
- elif base_file_path.startswith('/') or base_file_path.startswith('file:///'):
- base_file_path = re.sub(r'^(file:)?\/+', '/', base_file_path)
- elif re.match(r'^[a-zA-Z]+:\/\/', base_file_path):
- base_file_path = re.sub(r'^([a-zA-Z]+):\/\/+', '\\g<1>://', base_file_path)
- else:
- base_file_path = './' + re.sub(r'^(\.\/+)+', '', base_file_path)
- result_str = re.sub(r'\/+$', '', base_file_path) + '/' + re.sub(r'^(\.\/+)+', '', file_path)
- if not any(s for s in self.__file_prefix_allowlist if result_str.startswith(s)):
- result_str = ''
- return result_str
- def eval_sync(self, lines):
- """
- interpret a code
- """
- if isinstance(lines, str):
- line_array = lines.split("\n")
- elif isinstance(lines, list):
- line_array = lines
- else:
- return None
- for line in line_array:
- new_img_piece_size = {'x': 0, 'y':0}
- m = re.fullmatch(r'dir *: *([-_a-zA-Z0-9.\/%:]+) *', line)
- if m:
- self.dir_img = self.proper_file_path(m.group(1))
- if not self.is_dir_textimg_overridden:
- self.dir_textimg = self.dir_img
- continue
- m = re.fullmatch(r'dir *\( *textimg *\) *: *([-_a-zA-Z0-9.\/%:]+) *', line)
- if m:
- self.dir_textimg = self.proper_file_path(m.group(1))
- self.is_dir_textimg_overridden = True
- continue
- m = re.fullmatch(r'bg *: *([-_a-zA-Z0-9.\/%:]+) *', line)
- if m:
- bg_src = self.proper_file_path(m.group(1), self.dir_img)
- if os.path.isfile(bg_src):
- self.result_img = Image.open(bg_src).convert('RGBA')
- continue
- m = re.fullmatch(r'paste *(\(.*?\))? *: *([-_a-zA-Z0-9.\/%:]+) *', line)
- if m and self.result_img:
- file_path = self.proper_file_path(m.group(2), self.dir_img)
- if not os.path.isfile(file_path):
- continue
- paste_params = {'src' : file_path,
- 'x' : self.pos['x'],
- 'y' : self.pos['y']}
- paste_args = self.parse_args(m.group(1))
- if paste_args:
- args_length = len(paste_args)
- if re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[0]):
- paste_params['x'] += float(paste_args[0])
- if args_length > 1 and re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[1]):
- paste_params['y'] += float(paste_args[1])
- new_img_piece_size['x'], new_img_piece_size['y'] = self.paste_img(paste_params)
- for axis in ('x', 'y'):
- self.pos[axis] += new_img_piece_size[axis] * self.step[axis] + self.margin[axis]
- continue
- m = re.fullmatch(r'textimg *(\(.*?\))? *: *(.+)', line)
- if m and self.result_img:
- xpos_buff = self.pos['x'] # "textimg:" does not change pos['x']
- ypos_buff = self.pos['y'] # "textimg:" does not change pos['y']
- paste_args = self.parse_args(m.group(1))
- if paste_args:
- args_length = len(paste_args)
- if re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[0]):
- xpos_buff += float(paste_args[0])
- if args_length > 1 and re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[1]):
- ypos_buff += float(paste_args[1])
- textimg_text_body = m.group(2)
- for c in textimg_text_body:
- file_path = self.proper_file_path(f'textimg_{ord(c):x}.png', self.dir_textimg)
- if not os.path.isfile(file_path):
- continue
- paste_params = {'src' : file_path,
- 'x' : xpos_buff,
- 'y' : ypos_buff}
- new_img_piece_size['x'], new_img_piece_size['y'] = self.paste_img(paste_params)
- xpos_buff += new_img_piece_size['x'] # "textimg:" does not change pos['x']
- continue
- m = re.fullmatch(r'text *(\(.*?\))? *: *(.+)', line)
- if m and self.result_img:
- text_params = {'body' : m.group(2),
- 'x_offset' : 0,
- 'y_offset' : 0,
- 'size' : self.text_size,
- 'color' : self.text_color}
- text_args = self.parse_args(m.group(1))
- if text_args:
- args_length = len(text_args)
- if re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', text_args[0]):
- text_params['x_offset'] = float(text_args[0])
- if args_length > 1 and re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', text_args[1]):
- text_params['y_offset'] = float(text_args[1])
- if args_length > 2 and re.fullmatch(r'[0-9]+(\.[0-9]+)?', text_args[2]):
- text_params['size'] = int(text_args[2])
- if args_length > 3 and text_args[3].startswith('#'):
- matched_rgb = re.fullmatch(r'#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})', text_args[3])
- if matched_rgb:
- text_params['color'] = tuple(int(matched_rgb.group(i), 16) for i in range(1,4))
- img_draw = ImageDraw.Draw(self.result_img)
- try:
- img_draw.font = ImageFont.truetype(font=self.text_font, size=text_params['size'])
- except OSError:
- msg_template = [ ' ... try one of the following:',
- f'error: * install {self.__DEFAULT_FONT}',
- f'error: * put "{self.text_font}.ttf" somewhere under "/usr/share/fonts/truetype/" or some such',
- 'error: * specify a file path instead of a file name',
- 'error: * specify a file path instead of a font name',
- 'error: * specify any other available font']
- additional_err_msg = ''
- if self.text_font == self.__DEFAULT_FONT:
- additional_err_msg = f'{msg_template[0]}\n{msg_template[1]}\n{msg_template[2]}\n{msg_template[5]}'
- elif not '/' in self.text_font:
- if '.' in self.text_font:
- additional_err_msg = f'{msg_template[0]}\n{msg_template[3]}\n{msg_template[5]}'
- else:
- additional_err_msg = f'{msg_template[0]}\n{msg_template[2]}\n{msg_template[4]}\n{msg_template[5]}'
- print(f'error: **** cannot load font file ****', file=sys.stderr)
- print(f'error: could not load "{self.text_font}"{additional_err_msg}', file=sys.stderr)
- except Exception:
- print(f'error: **** cannot load font file ****', file=sys.stderr)
- print(f'error: (unknown error)', file=sys.stderr)
- try:
- img_draw.text((self.pos['x'] + text_params['x_offset'],
- round(self.pos['y'] + text_params['y_offset'] + text_params['size'] * 0.1)),
- text_params['body'],
- fill=text_params['color'])
- except UnicodeEncodeError:
- print(f'error: **** cannot draw text ****', file=sys.stderr)
- print(f'error: (maybe a font related problem or some sort of encoding problem)', file=sys.stderr)
- except Exception:
- print(f'error: **** cannot draw text ****', file=sys.stderr)
- print(f'error: (unknown error)', file=sys.stderr)
- self.pos['y'] += self.text_size * self.text_step + self.margin['y']
- continue
- m = re.fullmatch(r'text_([a-z]+) *: *(.+?) *', line)
- if m:
- if m.group(1) == 'font':
- self.text_font = m.group(2)
- elif m.group(1) == 'size':
- self.text_size = int(m.group(2))
- elif m.group(1) == 'color':
- if m.group(2).startswith('#'):
- regex = r'#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})'
- radix = 16
- else:
- regex = r'([0-9]+) *, *([0-9]+) *, *([0-9]+)'
- radix = 10
- matched_rgb = re.fullmatch(regex, m.group(2))
- if matched_rgb:
- self.text_color = tuple(int(matched_rgb.group(i), radix) for i in range(1,4))
- elif m.group(1) == 'step':
- self.text_step = float(m.group(2))
- continue
- m = re.fullmatch(r'(blank|margin|pos|step) *(\(.*?\))? *: *([-0-9.]+) *', line)
- if m:
- args = self.parse_args(m.group(2))
- if args and (args[0] not in ['x', 'y', '']):
- continue
- axes = ['x', 'y']
- if args and (args[0] in axes):
- axes = [args[0]]
- for axis in axes:
- if m.group(1) == 'blank' : self.pos[axis] += float(m.group(3))
- elif m.group(1) == 'margin' : self.margin[axis] = float(m.group(3))
- elif m.group(1) == 'pos' : self.pos[axis] = float(m.group(3))
- elif m.group(1) == 'step' : self.step[axis] = float(m.group(3))
- continue
- return None
- if __name__ == '__main__':
- if (len(sys.argv) == 1) or ((sys.argv[1] != '--stdin') and (not os.path.isfile(sys.argv[1]))):
- print('error: please specify a valid file name (or specify "--stdin")', file = sys.stderr)
- sys.exit(1)
- if sys.argv[1] == '--stdin':
- input_code_lines = tuple(sys.stdin)
- else:
- with open(sys.argv[1], mode='r', encoding='utf-8') as fin:
- input_code_lines = fin.readlines()
- main_img = GenerateImg()
- for input_code_line in input_code_lines:
- main_img.eval_sync(input_code_line)
- if not main_img:
- print('error: no background image', file=sys.stderr)
- sys.exit(2)
- if len(sys.argv) == 3 and sys.argv[2] == '--show':
- main_img.img.show()
- sys.exit()
- main_img.img.save('generated.png')
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement