diff --git a/README.md b/README.md index ed3b65ea..78a8b25e 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,8 @@ plugins: # #render_js: true #headless_chrome_path: headless-chromium + #mermaid_args: '-b transparent -t dark --scale 4 --quiet' + #mermaid_img_scale_reduction: 4 # #output_path: any-place/document.pdf #enabled_if_env: ENABLE_PDF_EXPORT @@ -245,6 +247,17 @@ plugins: > > ``` +* `mermaid_args` + + Arguments to use when calling `mmdc` to generate mermaid diagrams + + **default**: '-b transparent -t dark --scale 4 --quiet' + +* `mermaid_img_scale_reduction` + + Visual scale to reduce visual size of diagrams when using `--scale` greater than 1 in `mermaid_args`. + This allows higher resolution diagram renders at the native visual size. + * `relaxedjs_path` Set the value to execute command of relaxed if you're using e.g. '[Mermaid](https://mermaid-js.github.io) diagrams and Headless Chrome is not working for you. diff --git a/mkdocs_with_pdf/drivers/headless_chrome.py b/mkdocs_with_pdf/drivers/headless_chrome.py index 6f4debbc..c893e847 100644 --- a/mkdocs_with_pdf/drivers/headless_chrome.py +++ b/mkdocs_with_pdf/drivers/headless_chrome.py @@ -1,9 +1,7 @@ -import os -import html as html_lib +from ..utils.mermaid_util import render_mermaid from logging import Logger from shutil import which from subprocess import PIPE, Popen -import re import pathlib @@ -11,45 +9,39 @@ class HeadlessChromeDriver(object): """ 'Headless Chrome' executor """ @classmethod - def setup(self, program_path: str, logger: Logger): + def setup(self, + program_path: str, + mermaid_args: str, + mermaid_img_scale_reduction: float, + logger: Logger): if not which(program_path): raise RuntimeError( 'No such `Headless Chrome` program or not executable' + f': "{program_path}".') - return self(program_path, logger) + return self(program_path, + mermaid_args, + mermaid_img_scale_reduction, + logger) - def __init__(self, program_path: str, logger: Logger): + def __init__(self, program_path: str, + mermaid_args: str, + mermaid_img_scale_reduction: float, + logger: Logger): self._program_path = program_path + self.mermaid_args = mermaid_args + self.mermaid_img_scale_reduction = mermaid_img_scale_reduction self._logger = logger def render(self, html: str, temporary_directory: pathlib.Path) -> str: try: - mermaid_regex = r'
(.*?)
' - mermaid_matches = re.findall(mermaid_regex, html, flags=re.DOTALL) + html = render_mermaid( + html, + temporary_directory, + self.mermaid_args, + self.mermaid_img_scale_reduction, + self._logger) - # Convert each Mermaid diagram to an image. - for i, mermaid_code in enumerate(mermaid_matches): - self._logger.info(f"Converting mermaid diagram {i}") - - # Create a temporary file to hold the Mermaid code. - mermaid_file_path = temporary_directory / f"diagram_{i + 1}.mmd" - with open(mermaid_file_path, "wb") as mermaid_file: - mermaid_code_unescaped = html_lib.unescape(mermaid_code) - mermaid_file.write(mermaid_code_unescaped.encode("utf-8")) - - # Create a filename for the image. - image_file_path = temporary_directory / f"diagram_{i + 1}.png" - - # Convert the Mermaid diagram to an image using mmdc. - command = f"mmdc -i {mermaid_file_path} -o {image_file_path} -b transparent -t dark --scale 4 --quiet" - - os.system(command) - - # Replace the Mermaid code with the image in the HTML string. - image_html = f'Mermaid diagram {i+1}' - html = html.replace(f'
{mermaid_code}
', image_html) - - self._logger.info(f"Post mermaid translation: {html}") + self._logger.debug(f"Post mermaid translation: {html}") with open(temporary_directory / "post_mermaid_translation.html", "wb") as temp: temp.write(html.encode('utf-8')) @@ -59,14 +51,13 @@ def render(self, html: str, temporary_directory: pathlib.Path) -> str: '--no-sandbox', '--headless', '--disable-gpu', - '--disable-web-security', '-–allow-file-access-from-files', '--run-all-compositor-stages-before-draw', '--virtual-time-budget=10000', '--dump-dom', temp.name], stdout=PIPE) as chrome: chrome_output = chrome.stdout.read().decode('utf-8') - self._logger.info(f"Post chrome translation: {chrome_output}") + self._logger.debug(f"Post chrome translation: {chrome_output}") return chrome_output except Exception as e: diff --git a/mkdocs_with_pdf/drivers/relaxedjs.py b/mkdocs_with_pdf/drivers/relaxedjs.py index a0177ad2..5490ff3e 100644 --- a/mkdocs_with_pdf/drivers/relaxedjs.py +++ b/mkdocs_with_pdf/drivers/relaxedjs.py @@ -1,14 +1,20 @@ import os +from ..utils.mermaid_util import render_mermaid from logging import Logger from shutil import which from subprocess import PIPE, Popen from tempfile import TemporaryDirectory +import pathlib class RelaxedJSRenderer(object): @classmethod - def setup(self, program_path: str, logger: Logger): + def setup(self, + program_path: str, + mermaid_args: str, + mermaid_img_scale_reduction: float, + logger: Logger): if not program_path: return None @@ -17,16 +23,33 @@ def setup(self, program_path: str, logger: Logger): 'No such `ReLaXed` program or not executable' + f': "{program_path}".') - return self(program_path, logger) + return self(program_path, + mermaid_args, + mermaid_img_scale_reduction, + logger) - def __init__(self, program_path: str, logger: Logger): + def __init__(self, program_path: str, + mermaid_args: str, + mermaid_img_scale_reduction: float, + logger: Logger): self._program_path = program_path + self.mermaid_args = mermaid_args + self.mermaid_img_scale_reduction = mermaid_img_scale_reduction self._logger = logger - def write_pdf(self, html_string: str, output: str): + def write_pdf(self, html_string: str, + output: str, + temporary_directory: pathlib.Path): self._logger.info(' Rendering with `ReLaXed JS`.') with TemporaryDirectory() as work_dir: + html_string = render_mermaid( + html_string, + work_dir, + self.mermaid_args, + self.mermaid_img_scale_reduction, + self._logger) + entry_point = os.path.join(work_dir, 'pdf_print.html') with open(entry_point, 'w+') as f: f.write(html_string) @@ -42,3 +65,7 @@ def write_pdf(self, html_string: str, output: str): self._logger.info(f" {log}") if proc.poll() is not None: break + # workaround for '--build-once' not working + if log.find("Now idle and waiting for file changes") > -1: + proc.kill() + break diff --git a/mkdocs_with_pdf/generator.py b/mkdocs_with_pdf/generator.py index 195528cf..2e38de29 100644 --- a/mkdocs_with_pdf/generator.py +++ b/mkdocs_with_pdf/generator.py @@ -163,7 +163,7 @@ def add_stylesheet(stylesheet: str): if self._options.relaxed_js: self._options.relaxed_js.write_pdf( - html_string, abs_pdf_path) + html_string, abs_pdf_path, temporary_directory) else: html = HTML(string=html_string) render = html.render() @@ -397,7 +397,6 @@ def _render_js(self, soup, temporary_directory: pathlib.Path): body.append(tag) for src in scripts: body.append(soup.new_tag('script', src=f'file://{src}')) - return self._options.js_renderer.render(str(soup), temporary_directory) def _scrap_scripts(self, soup): diff --git a/mkdocs_with_pdf/options.py b/mkdocs_with_pdf/options.py index 8296786e..f3381d59 100644 --- a/mkdocs_with_pdf/options.py +++ b/mkdocs_with_pdf/options.py @@ -42,6 +42,8 @@ class Options(object): ('two_columns_level', config_options.Type(int, default=0)), ('render_js', config_options.Type(bool, default=False)), + ('mermaid_args', config_options.Type(str, default="-b transparent -t dark --scale 4 --quiet")), + ('mermaid_img_scale_reduction', config_options.Type((float, int), default=1)), ('headless_chrome_path', config_options.Type(str, default='chromium-browser')), ('relaxedjs_path', @@ -96,10 +98,16 @@ def __init__(self, local_config, config, logger: logging): self.js_renderer = None if local_config['render_js']: self.js_renderer = HeadlessChromeDriver.setup( - local_config['headless_chrome_path'], logger) + local_config['headless_chrome_path'], + local_config['mermaid_args'], + local_config['mermaid_img_scale_reduction'], + logger) self.relaxed_js = RelaxedJSRenderer.setup( - local_config['relaxedjs_path'], logger) + local_config['relaxedjs_path'], + local_config['mermaid_args'], + local_config['mermaid_img_scale_reduction'], + logger) # Theming self.theme_name = config['theme'].name diff --git a/mkdocs_with_pdf/utils/image_util.py b/mkdocs_with_pdf/utils/image_util.py index 3e73506d..21d69b4d 100644 --- a/mkdocs_with_pdf/utils/image_util.py +++ b/mkdocs_with_pdf/utils/image_util.py @@ -15,7 +15,12 @@ def fix_image_alignment(soup: PageElement, logger: Logger = None): if img.has_attr('class') and 'twemoji' in img['class']: continue - styles = _parse_style(getattr(img, 'style', '')) + if not (img.has_attr('align') + or img.has_attr('width') + or img.has_attr('height')): + continue + + styles = _parse_style(img.get('style', '')) logger.debug(f' | {img}') if img.has_attr('align'): diff --git a/mkdocs_with_pdf/utils/mermaid_util.py b/mkdocs_with_pdf/utils/mermaid_util.py new file mode 100644 index 00000000..d9585669 --- /dev/null +++ b/mkdocs_with_pdf/utils/mermaid_util.py @@ -0,0 +1,63 @@ +import os +import re +import html as html_lib +import pathlib +from logging import Logger +from typing import Union + + +def render_mermaid(html: str, + temporary_directory: Union[str, pathlib.Path], + mermaid_args: str, + mermaid_img_scale_reduction: float, + logger: Logger): + + if (isinstance(temporary_directory, str)): + temporary_directory = pathlib.Path(temporary_directory) + + mermaid_regex = re.compile(r'<(\w*?[^>]*)(><[^>]*?|[^>]*?)class="[^>\"]*(language-)?mermaid[^>\"]*">(<[^>]*?>)?(?P.*?)(<\/[^>]*?>)?<\/\1>', flags=re.DOTALL) + mermaid_matches = mermaid_regex.finditer(html) + + i = 0 + # Convert each Mermaid diagram to an image. + for mermaid_block in mermaid_matches: + i += 1 + logger.info(f"Converting mermaid diagram {i}") + mermaid_code = mermaid_block.group("code") + + # Create a temporary file to hold the Mermaid code. + mermaid_file_path = temporary_directory / f"diagram_{i + 1}.mmd" + with open(mermaid_file_path, "wb") as mermaid_file: + mermaid_code_unescaped = html_lib.unescape(mermaid_code) + mermaid_file.write(mermaid_code_unescaped.encode("utf-8")) + + # Create a filename for the image. + image_file_path = temporary_directory / f"diagram_{i}.png" + + # Convert the Mermaid diagram to an image using mmdc. + command = f"mmdc -i {mermaid_file_path} -o {image_file_path} {mermaid_args}" + + # suppress sub-process chatter when using '--quiet' + if mermaid_args.find('--quiet') > -1 or mermaid_args.find(' -q ') > -1 or mermaid_args.endswith(' -q'): + command += " >/dev/null 2>&1" + + os.system(command) + + if not os.path.exists(image_file_path): + logger.warning(f"Error: Failed to generate mermaid diagram {i}") + else: + from PIL import Image + + with Image.open(image_file_path) as im: + # Replace the Mermaid code with the image in the HTML string. + image_html = f'Mermaid diagram {i}' + + if mermaid_img_scale_reduction != 1: + height = im.height // mermaid_img_scale_reduction + width = im.width // mermaid_img_scale_reduction + image_html = image_html.replace('">', f'" style="max-width:{width}px; max-height:{height}px;">') + + html = html.replace(mermaid_block.group(0), + mermaid_block.group(0).replace(mermaid_code, image_html)) + + return html