Advertisement
Guest User

generate_img.py 0.0.99.20211018 (2021-10-18) by cleemy desu wayo

a guest
Oct 18th, 2021
76
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 13.78 KB | None | 0 0
  1. #!/usr/bin/python3
  2. """
  3. generate_img.py version 0.0.99.20211018 (2021-10-18) written by cleemy desu wayo
  4.  
  5. this is a personal work and is a PDS (Public Domain Software)
  6.  
  7. --------
  8. requirements: Python 3.8 or later, PIL (Pillow), GNU Unifont
  9. (and maybe this code works under only Linux)
  10.  
  11. on Ubuntu, GNU Unifont becomes available by doing "sudo apt-get install unifont"
  12. --------
  13.  
  14. see https://twitter.com/cleemy4545/status/1450188684045787142 and the thread
  15.  
  16. old version: https://pastebin.com/JJPXFtfW
  17. """
  18.  
  19. import os
  20. import sys
  21. import re
  22.  
  23. from unicodedata import category
  24. from PIL import Image, ImageDraw, ImageFont
  25.  
  26. class GenerateImg:
  27.   """
  28.  main class of generate_img.py
  29.  """
  30.  
  31.   @property
  32.   def __version__(self):
  33.     return '0.0.99.xx'
  34.  
  35.   def __init__(self):
  36.     self.__DEFAULT_FONT          = 'unifont'
  37.     self.__file_prefix_allowlist = ('./', '/')
  38.  
  39.     self.result_img                = None
  40.     self.dir_img                   = './'
  41.     self.dir_textimg               = './'
  42.     self.is_dir_textimg_overridden = False
  43.     self.pos            = {'x': 0, 'y':0}
  44.     self.margin         = {'x': 0, 'y':0}
  45.     self.step           = {'x': 1, 'y':0}
  46.     self.text_size      = 16
  47.     self.text_color     = (0,0,0)
  48.     self.text_font      = self.__DEFAULT_FONT
  49.     self.text_step      = 1
  50.  
  51.   def __len__(self):
  52.     return self.width
  53.  
  54.   @property
  55.   def img(self):
  56.     """
  57.    return main image object
  58.    """
  59.     return self.result_img
  60.  
  61.   @property
  62.   def width(self):
  63.     """
  64.    return width of main image object
  65.    (return 0 if image is None)
  66.    """
  67.     if self.result_img is None:
  68.       return 0
  69.  
  70.     return self.result_img.width
  71.  
  72.   @property
  73.   def height(self):
  74.     """
  75.    return height of main image object
  76.    (return 0 if image is None)
  77.    """
  78.     if self.result_img is None:
  79.       return 0
  80.  
  81.     return self.result_img.height
  82.  
  83.   @property
  84.   def file_prefix_allowlist(self):
  85.     """
  86.    return an allowlist of file prefix
  87.    """
  88.     return self.__file_prefix_allowlist
  89.  
  90.   def paste_img(self, params):
  91.     """
  92.    draw new image and return a tuple (loaded image size)
  93.    """
  94.  
  95.     img_piece = Image.open(params['src']).convert('RGBA')
  96.     img_tmp_canvas = Image.new('RGBA', self.result_img.size, (255, 255, 255, 0))
  97.     img_tmp_canvas.paste(img_piece, (round(params['x']), round(params['y'])))
  98.     self.result_img = Image.alpha_composite(self.result_img, img_tmp_canvas)
  99.  
  100.     return img_piece.size
  101.  
  102.   @classmethod
  103.   def parse_args(cls, source_str):
  104.     """
  105.    parse source_str and return list
  106.  
  107.    '(100,100,20,#ffffff)'      --> return ['100', '100', '20', '#ffffff']
  108.    '(100)'                     --> return ['100']
  109.    '   (100)   '               --> return ['100']
  110.    '(   100  ,   100   )'      --> return ['100', '100']
  111.    '(   100  ,   10 0  )'      --> return ['100', '10 0']
  112.    '(,,20)'                    --> return ['', '', '20']
  113.    '(20,,)'                    --> return ['20', '', '']
  114.    '(,,,)'                     --> return ['', '', '', '']
  115.    '( , , , )'                 --> return ['', '', '', '']
  116.    '()'                        --> return ['']
  117.    '( )'                       --> return ['']
  118.    ''                          --> return []
  119.    '(())'                      --> return ['()']
  120.    '(100,100' (invalid string) --> return []
  121.    [100, 100] (not string)     --> return []
  122.    """
  123.  
  124.     if not isinstance(source_str, str):
  125.       return []
  126.  
  127.     args_str = re.sub(r'^\((.*)\)$', '\\g<1>', source_str.strip())  # ' (100,100) ' --> '100,100'
  128.     if args_str == source_str:
  129.       return []
  130.  
  131.     return [s.strip() for s in args_str.split(',')]
  132.  
  133.   def proper_file_path(self, file_path, base_file_path_src = ''):
  134.     """
  135.    return a proper file path
  136.  
  137.    this ignores base_file_path_src if file_path starts with '/', 'file:///',
  138.    'https://', 'ftp://' or some such
  139.    """
  140.  
  141.     # has a control code?
  142.     if any(s for s in f'{file_path}{base_file_path_src}' if category(s) == 'Cc'):
  143.       return ''
  144.  
  145.     result_str = ''
  146.  
  147.     if file_path.startswith('/') or file_path.startswith('file:///'):
  148.       result_str = re.sub(r'^(file:)?\/+', '/', file_path)
  149.     elif re.match(r'^[a-zA-Z]+:\/\/', file_path):
  150.       result_str = re.sub(r'^([a-zA-Z]+):\/\/+', '\\g<1>://', file_path)
  151.     else:
  152.  
  153.       base_file_path = f'{base_file_path_src}'
  154.  
  155.       if (base_file_path_src is None) or (base_file_path == ''):
  156.         base_file_path = '.'
  157.       elif base_file_path.startswith('/') or base_file_path.startswith('file:///'):
  158.         base_file_path = re.sub(r'^(file:)?\/+', '/', base_file_path)
  159.       elif re.match(r'^[a-zA-Z]+:\/\/', base_file_path):
  160.         base_file_path = re.sub(r'^([a-zA-Z]+):\/\/+', '\\g<1>://', base_file_path)
  161.       else:
  162.         base_file_path = './' + re.sub(r'^(\.\/+)+', '', base_file_path)
  163.  
  164.       result_str = re.sub(r'\/+$', '', base_file_path) + '/' + re.sub(r'^(\.\/+)+', '', file_path)
  165.  
  166.     if not any(s for s in self.__file_prefix_allowlist if result_str.startswith(s)):
  167.       result_str = ''
  168.  
  169.     return result_str
  170.  
  171.   def eval_sync(self, lines):
  172.     """
  173.    interpret a code
  174.    """
  175.  
  176.     if isinstance(lines, str):
  177.       line_array = lines.split("\n")
  178.     elif isinstance(lines, list):
  179.       line_array = lines
  180.     else:
  181.       return None
  182.  
  183.     for line in line_array:
  184.       new_img_piece_size = {'x': 0, 'y':0}
  185.  
  186.       m = re.fullmatch(r'dir *: *([-_a-zA-Z0-9.\/%:]+) *', line)
  187.       if m:
  188.         self.dir_img = self.proper_file_path(m.group(1))
  189.         if not self.is_dir_textimg_overridden:
  190.           self.dir_textimg = self.dir_img
  191.         continue
  192.  
  193.       m = re.fullmatch(r'dir *\( *textimg *\) *: *([-_a-zA-Z0-9.\/%:]+) *', line)
  194.       if m:
  195.         self.dir_textimg = self.proper_file_path(m.group(1))
  196.         self.is_dir_textimg_overridden = True
  197.         continue
  198.  
  199.       m = re.fullmatch(r'bg *: *([-_a-zA-Z0-9.\/%:]+) *', line)
  200.       if m:
  201.         bg_src = self.proper_file_path(m.group(1), self.dir_img)
  202.         if os.path.isfile(bg_src):
  203.           self.result_img = Image.open(bg_src).convert('RGBA')
  204.         continue
  205.  
  206.       m = re.fullmatch(r'paste *(\(.*?\))? *: *([-_a-zA-Z0-9.\/%:]+) *', line)
  207.       if m and self.result_img:
  208.  
  209.         file_path = self.proper_file_path(m.group(2), self.dir_img)
  210.         if not os.path.isfile(file_path):
  211.           continue
  212.  
  213.         paste_params = {'src' : file_path,
  214.                         'x'   : self.pos['x'],
  215.                         'y'   : self.pos['y']}
  216.  
  217.         paste_args = self.parse_args(m.group(1))
  218.         if paste_args:
  219.           args_length = len(paste_args)
  220.           if re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[0]):
  221.             paste_params['x'] += float(paste_args[0])
  222.           if args_length > 1 and re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[1]):
  223.             paste_params['y'] += float(paste_args[1])
  224.  
  225.         new_img_piece_size['x'], new_img_piece_size['y'] = self.paste_img(paste_params)
  226.  
  227.         for axis in ('x', 'y'):
  228.           self.pos[axis] += new_img_piece_size[axis] * self.step[axis] + self.margin[axis]
  229.         continue
  230.  
  231.       m = re.fullmatch(r'textimg *(\(.*?\))? *: *(.+)', line)
  232.       if m and self.result_img:
  233.         xpos_buff = self.pos['x']    # "textimg:" does not change pos['x']
  234.         ypos_buff = self.pos['y']    # "textimg:" does not change pos['y']
  235.  
  236.         paste_args = self.parse_args(m.group(1))
  237.         if paste_args:
  238.           args_length = len(paste_args)
  239.           if re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[0]):
  240.             xpos_buff += float(paste_args[0])
  241.           if args_length > 1 and re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', paste_args[1]):
  242.             ypos_buff += float(paste_args[1])
  243.  
  244.         textimg_text_body = m.group(2)
  245.         for c in textimg_text_body:
  246.           file_path = self.proper_file_path(f'textimg_{ord(c):x}.png', self.dir_textimg)
  247.           if not os.path.isfile(file_path):
  248.             continue
  249.  
  250.           paste_params = {'src' : file_path,
  251.                           'x'   : xpos_buff,
  252.                           'y'   : ypos_buff}
  253.  
  254.           new_img_piece_size['x'], new_img_piece_size['y'] = self.paste_img(paste_params)
  255.  
  256.           xpos_buff += new_img_piece_size['x']    # "textimg:" does not change pos['x']
  257.  
  258.         continue
  259.  
  260.       m = re.fullmatch(r'text *(\(.*?\))? *: *(.+)', line)
  261.       if m and self.result_img:
  262.         text_params = {'body'     : m.group(2),
  263.                        'x_offset' : 0,
  264.                        'y_offset' : 0,
  265.                        'size'     : self.text_size,
  266.                        'color'    : self.text_color}
  267.  
  268.         text_args = self.parse_args(m.group(1))
  269.         if text_args:
  270.           args_length = len(text_args)
  271.           if re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', text_args[0]):
  272.             text_params['x_offset'] = float(text_args[0])
  273.           if args_length > 1 and re.fullmatch(r'-?[0-9]+(\.[0-9]+)?', text_args[1]):
  274.             text_params['y_offset'] = float(text_args[1])
  275.           if args_length > 2 and re.fullmatch(r'[0-9]+(\.[0-9]+)?', text_args[2]):
  276.             text_params['size'] = int(text_args[2])
  277.           if args_length > 3 and text_args[3].startswith('#'):
  278.             matched_rgb = re.fullmatch(r'#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})', text_args[3])
  279.             if matched_rgb:
  280.               text_params['color'] = tuple(int(matched_rgb.group(i), 16) for i in range(1,4))
  281.  
  282.         img_draw = ImageDraw.Draw(self.result_img)
  283.  
  284.         try:
  285.           img_draw.font = ImageFont.truetype(font=self.text_font, size=text_params['size'])
  286.         except OSError:
  287.           msg_template = [ ' ... try one of the following:',
  288.                           f'error:     * install {self.__DEFAULT_FONT}',
  289.                           f'error:     * put "{self.text_font}.ttf" somewhere under "/usr/share/fonts/truetype/" or some such',
  290.                            'error:     * specify a file path instead of a file name',
  291.                            'error:     * specify a file path instead of a font name',
  292.                            'error:     * specify any other available font']
  293.  
  294.           additional_err_msg = ''
  295.           if self.text_font == self.__DEFAULT_FONT:
  296.             additional_err_msg = f'{msg_template[0]}\n{msg_template[1]}\n{msg_template[2]}\n{msg_template[5]}'
  297.           elif not '/' in self.text_font:
  298.             if '.' in self.text_font:
  299.               additional_err_msg = f'{msg_template[0]}\n{msg_template[3]}\n{msg_template[5]}'
  300.             else:
  301.               additional_err_msg = f'{msg_template[0]}\n{msg_template[2]}\n{msg_template[4]}\n{msg_template[5]}'
  302.  
  303.           print(f'error: **** cannot load font file ****', file=sys.stderr)
  304.           print(f'error:   could not load "{self.text_font}"{additional_err_msg}', file=sys.stderr)
  305.         except Exception:
  306.           print(f'error: **** cannot load font file ****', file=sys.stderr)
  307.           print(f'error:   (unknown error)', file=sys.stderr)
  308.  
  309.         try:
  310.           img_draw.text((self.pos['x'] + text_params['x_offset'],
  311.                          round(self.pos['y'] + text_params['y_offset'] + text_params['size'] * 0.1)),
  312.                         text_params['body'],
  313.                         fill=text_params['color'])
  314.         except UnicodeEncodeError:
  315.           print(f'error: **** cannot draw text ****', file=sys.stderr)
  316.           print(f'error:   (maybe a font related problem or some sort of encoding problem)', file=sys.stderr)
  317.         except Exception:
  318.           print(f'error: **** cannot draw text ****', file=sys.stderr)
  319.           print(f'error:   (unknown error)', file=sys.stderr)
  320.  
  321.         self.pos['y'] += self.text_size * self.text_step + self.margin['y']
  322.         continue
  323.  
  324.       m = re.fullmatch(r'text_([a-z]+) *: *(.+?) *', line)
  325.       if m:
  326.         if m.group(1) == 'font':
  327.           self.text_font = m.group(2)
  328.         elif m.group(1) == 'size':
  329.           self.text_size = int(m.group(2))
  330.         elif m.group(1) == 'color':
  331.           if m.group(2).startswith('#'):
  332.             regex = r'#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})'
  333.             radix = 16
  334.           else:
  335.             regex = r'([0-9]+) *, *([0-9]+) *, *([0-9]+)'
  336.             radix = 10
  337.  
  338.           matched_rgb = re.fullmatch(regex, m.group(2))
  339.           if matched_rgb:
  340.             self.text_color = tuple(int(matched_rgb.group(i), radix) for i in range(1,4))
  341.  
  342.         elif m.group(1) == 'step':
  343.           self.text_step = float(m.group(2))
  344.         continue
  345.  
  346.       m = re.fullmatch(r'(blank|margin|pos|step) *(\(.*?\))? *: *([-0-9.]+) *', line)
  347.       if m:
  348.         args = self.parse_args(m.group(2))
  349.         if args and (args[0] not in ['x', 'y', '']):
  350.           continue
  351.  
  352.         axes = ['x', 'y']
  353.         if args and (args[0] in axes):
  354.           axes = [args[0]]
  355.  
  356.         for axis in axes:
  357.           if   m.group(1) == 'blank'  : self.pos[axis]    += float(m.group(3))
  358.           elif m.group(1) == 'margin' : self.margin[axis]  = float(m.group(3))
  359.           elif m.group(1) == 'pos'    : self.pos[axis]     = float(m.group(3))
  360.           elif m.group(1) == 'step'   : self.step[axis]    = float(m.group(3))
  361.         continue
  362.  
  363.     return None
  364.  
  365. if __name__ == '__main__':
  366.  
  367.   if (len(sys.argv) == 1) or ((sys.argv[1] != '--stdin') and (not os.path.isfile(sys.argv[1]))):
  368.     print('error: please specify a valid file name (or specify "--stdin")', file = sys.stderr)
  369.     sys.exit(1)
  370.  
  371.   if sys.argv[1] == '--stdin':
  372.     input_code_lines = tuple(sys.stdin)
  373.   else:
  374.     with open(sys.argv[1], mode='r', encoding='utf-8') as fin:
  375.       input_code_lines = fin.readlines()
  376.  
  377.   main_img = GenerateImg()
  378.  
  379.   for input_code_line in input_code_lines:
  380.     main_img.eval_sync(input_code_line)
  381.  
  382.   if not main_img:
  383.     print('error: no background image', file=sys.stderr)
  384.     sys.exit(2)
  385.  
  386.   if len(sys.argv) == 3 and sys.argv[2] == '--show':
  387.     main_img.img.show()
  388.     sys.exit()
  389.  
  390.   main_img.img.save('generated.png')
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement