""" This module contains utility functions for generating HTML content to include in your newsletters. > df_to_html() and produce_images() functions leverage the [jinja2](https://pypi.org/project/jinja2) package to generate the HTML content from templates. > Feel free to reach out if you need help to start with jinja2. To use this module in your script, import the module like so. ``` from djagitit.mailing import html ``` """ import pkg_resources from jinja2 import Environment, FileSystemLoader import pandas as pd from dotenv import load_dotenv import pathlib import numpy as np from html2image import Html2Image import base64 from PIL import Image import os import random load_dotenv() _imgDir = pkg_resources.resource_filename(__name__, '../ressources/img/') def image_to_base64(img_path, width=None): """ Convert an image to a base64 encoded string. Args: img_path (str): The path to the image file. width (int, optional): The width to resize the image to. Defaults to None. Returns: encoded_string (str): The base64 encoded string of the image. Example: ``` #to convert an image 'shook_ones.png' located in the 'img' folder html.image_to_base64("img/shook_ones.png") # if you want to resize the image to be 50px wide before converting it html.image_to_base64("img/shook_ones.png", 50) ``` """ with open(img_path, "rb") as image_file: img = Image.open(image_file) # If width is specified, resize the image if width is not None: wpercent = (width / float(img.size[0])) hsize = int(float(img.size[1]) * float(wpercent)) img = img.resize((width, hsize), Image.LANCZOS) # Save the image to a temporary file imgKey = ''.join(random.choices('0123456789ABCDEF', k=10)) temp_img_path = f"{_imgDir}{imgKey}.png" img.save(temp_img_path) # Convert the image to base64 encoded_string = base64.b64encode(open(temp_img_path, "rb").read()).decode('utf-8') # Remove the temporary file os.remove(temp_img_path) return encoded_string class Logos: """ Utility class for generating logos in base64 encoded strings to include in your HTML content. The list of available logos is [here](https://github.com/SME-BUS/cea-foobar/tree/main/packages/djagitit/ressources/img) Example: ``` #to use the sonymusic.png image in your HTML content html.Logos.sonymusic() ``` ``` #to use the spotify.png image and resize it to 50px width by keeping aspect ratio html.Logos.spotify(width=50) ``` """ @classmethod def _create_method(cls, name): @classmethod def _method(cls, width=None): return image_to_base64(f"{_imgDir}{name}.png", width=width) return _method logosList = [f.split(".")[0] for f in os.listdir(_imgDir) if f.endswith('.png')] for logoName in logosList: setattr(Logos, logoName, Logos._create_method(logoName)) def __process_screenshot(html, output_dir, output_filename, output_width): '''Process a screenshot of an HTML page. Args: html (str): The HTML content to process. output_dir (str): The directory where the output image will be saved. output_filename (str): The name of the output image file. output_width (int): The desired width of the output image. Returns: str: The base64 encoded string of the processed image. ''' # Create an instance of Html2Image hti = Html2Image(output_path=output_dir, temp_path=os.path.join(output_dir, 'tmp')) # Screenshot the HTML content hti.screenshot(html_str=html, save_as=f'{output_filename}.png') # We want to crop the screenshot to the content area only (remove transparent area) img = np.array(Image.open(f'{output_dir}/{output_filename}.png')) # img is 1080 rows by 1920 cols and 4 color channels, the 4'th channel is alpha. idx = np.where(img[:, :, 3] > 0) # Find indices of non-transparent pixels (indices where alpha channel value is above zero). x0, y0, x1, y1 = idx[1].min(), idx[0].min(), idx[1].max(), idx[0].max() # Get minimum and maximum index in both axes (top left corner and bottom right corner) img = Image.fromarray(img[y0:y1+1, x0:x1+1, :]) # Crop rectangle and convert to Image img.save(f'{output_dir}/{output_filename}.png') # save cropped image # Convert the image to base64 string img64 = image_to_base64(f'{output_dir}/{output_filename}.png', width=output_width) # Remove the temporary image os.remove(f'{output_dir}/{output_filename}.png') return img64 def produce_images(template, df, image_directory, img_name='img', params=None, output_width=800, limit=None): ''' Produce images for each row of a dataframeusing a html jinja template and add them to a dataframe. Args: template (str): The path to the jinja template file. df (pandas.DataFrame): The dataframe containing the data to be used in the template. image_directory (str): The directory where the output images will be saved. img_name (str, optional): The name of the output image temporary file. This will be used as the column name to add in the dataframe. Defaults to 'img'. params (dict, optional): Additional parameters to be passed to the template. Defaults to None. output_width (int, optional): The desired width of the output images. Defaults to 800. limit (int, optional): The maximum number of images to produce. Defaults to None. Returns: df (pandas.DataFrame): The dataframe with base64 encoded images added as a new column (named after the img_name). ''' # load template env = Environment(loader=FileSystemLoader(f'{pathlib.Path(template).parent.absolute()}')) template = env.get_template(pathlib.Path(template).name) # if a limit is specified use it limit = len(df) if limit is None else limit for index, row in df[:limit].iterrows(): # set the template parameters render_params = {} render_params.update({'row': row.to_dict()}) render_params.update(params) if params is not None else None #render the template img = template.render(render_params) # convert the generated image to base64 imgBase64 = __process_screenshot(html=img, output_dir=image_directory, output_filename=img_name, output_width=output_width) # update the dataframe with the base64 image df.loc[index, img_name] = imgBase64 return df def df_to_html(template, df, params=None, write_to_file=None): ''' Convert a dataframe to html using a jinja template. Args: template (str): The path to the jinja template file. df (pandas.DataFrame): The dataframe containing the data to be used in the template. params (dict, optional): Additional parameters to be passed to the template. Defaults to None. write_to_file (str, optional): The path to the file to write the html to. Defaults to None and will not write to a file. Returns: html (str): The html content as a string ''' #load template jinja_env = Environment(loader=FileSystemLoader(f'{pathlib.Path(template).parent.absolute()}')) html_template = jinja_env.get_template(pathlib.Path(template).name) #set the template parameters render_params = {} render_params.update(params) if params is not None else None #if a dict of dataframes is passed, add each dataframe to the template parameters if isinstance(df, dict): for key, value in df.items(): render_params.update({key: value.to_dict(orient='records')}) #else add the single dataframe passed to the template parameters else: render_params.update({'df': df.to_dict(orient='records')}) #render the template html = html_template.render(render_params) #if a file is specified, write the html to the file if write_to_file: with open(write_to_file, 'w') as f: f.write(html) #return the html return html