from __future__ import annotations
import io
import PIL.Image
import numpy as np
from scistag.imagestag import Color
from scistag.imagestag.image import Image
try:
from scistag.third_party.imgkit_fix import from_string, from_url
IMG_KIT_AVAILABLE = True
except ModuleNotFoundError:
IMG_KIT_AVAILABLE = False
[docs]class HtmlRenderer:
"""
The HtmlRenderer allows the conversion from HTML code to an image for
example to create the thumbnail of a web page or to integrate HTML elements
into a slide.
Important: Needs te optional imgkit module and wkhtmltopdf
(https://wkhtmltopdf.org/)
"""
def __init__(self):
"""
Initializer
"""
self.image: np.ndarray | None = None
"""
The rendered web page
"""
[docs] @staticmethod
def dummy_image() -> Image:
"""
Returns a dummy image if not data is available
:return: Dummy image in the current framework's representation
"""
result = np.ones((1, 1, 3), dtype=np.uint8)
return Image(result)
[docs] def render(self, options: dict | None = None) -> Image:
"""
Renders the HTML code to an image
:param options: One of the following optins can be provided:
* html - The website's full HTML code (if set body & style are
ignored)
* body - Only the website's body. You can pass only the content
code or
the full <body> block
* style - The site's style configuration (head.style)
* width - The rendering width. Otherwise auto detected / clipped at
1024. None by default.
* height - The rendering height. Otherwise auto detected / no
limitation. None by default.
* transparent - Defines if the page shall be rendered onto
transparent background so an RGBA image will be
created. False by default.
* backgroundColor - The color as Color object. White by default.
* trimWidth - Automatically reduce the image's width if the full
extend is not used. True by default.
* trimHeight - Defines if the height shall be trimmed. False by
default
:return: An image handle
"""
if not IMG_KIT_AVAILABLE:
self.dummy_image()
options = options if options is not None else {}
quality = options.get("quality", 100)
transparent = options.get("transparent", False)
background_color = \
options.get("backgroundColor",
Color(1.0, 1.0, 1.0, 0.0 if transparent else 1.0))
if not transparent and background_color.to_rgba()[3] < 1.0:
transparent = True
image_format = options.get("format",
"bmp" if not transparent else "png")
red, green, blue, alpha = background_color.to_int_rgba()
bg_color = f'rgba({red},{green},{blue},{alpha / 255})'
html_data = self._generate_html(bg_color, options)
html_options = {"format": image_format, "quality": quality,
"quiet": None}
trim_width, trim_height = options.get("trimWidth", True), options.get(
"trimHeight", False)
if "width" in options:
html_options["width"] = options["width"]
if "height" in options:
html_options["height"] = options["height"]
if transparent:
html_options["transparent"] = None
image = from_string(html_data, False, options=html_options)
pil_image = PIL.Image.open(io.BytesIO(image))
if trim_width or trim_height:
image = self._trim(np.array(pil_image), transparent=transparent,
trim_width=trim_width,
trim_height=trim_height)
return Image(image)
return Image(pil_image)
[docs] @staticmethod
def _trim(image: np.ndarray, transparent: bool, trim_width=True,
trim_height=False) -> np.ndarray:
"""
Searches for the first and last non-empty rows and columns and reduces
the image to the valid pixels.
:param image: The original image
:param transparent: Defines if the image was transparent and the alpha
channel can be used
:param trim_width: Defines if the image shall be reduced horizontally
:param trim_height: Defines if the image shall be reduced vertically
:return: The trimmed image
"""
if transparent and image.shape[2] == 4:
alpha_channel: np.ndarray = image[:, :, 3]
search_value = 255
else:
alpha_channel = Image.normalize_to_gray(image)
search_value = alpha_channel[-1, -1]
row_sums = np.sum(alpha_channel, axis=1)
col_sums = np.sum(alpha_channel, axis=0)
if trim_height:
row_mismatch = np.where(
row_sums != alpha_channel.shape[1] * search_value)
first_row = row_mismatch[0][0] if len(row_mismatch) != 0 else 0
last_row = row_mismatch[0][-1] if len(row_mismatch) != 0 else \
image.shape[1] - 1
else:
first_row = 0
last_row = image.shape[0] - 1
if trim_width:
col_mismatch = np.where(
col_sums != alpha_channel.shape[0] * search_value)
first_col = col_mismatch[0][0] if len(col_mismatch) != 0 else 0
last_col = col_mismatch[0][-1] if len(col_mismatch) != 0 else \
image.shape[0] - 1
else:
first_col = 0
last_col = image.shape[1] - 1
return image[first_row:last_row + 1, first_col:last_col + 1, :]
[docs] @staticmethod
def _generate_html(bg_color, options):
"""
Generates the html data
:param bg_color: The background color
:param options: Generator options. See render
:return: The html code
"""
html = options.get("html", None)
body = options.get("body", "")
if html is not None:
html_data = html
else:
style = options.get("style", "")
html_data = f'<html><style>{style}</style>'
if body.startswith("<body>"):
html_data += body + "</html>"
else:
html_data += f'<body style="padding:0;margin:0;' \
f'background-color:{bg_color};">' + body + \
'</body></html>'
return html_data