pip install markdown imgkitYou'll also need to install wkhtmltopdf on your system:
On Ubuntu/Debian: sudo apt-get install wkhtmltopdf
On macOS: brew install wkhtmltopdf
On Windows: Download the installer from the wkhtmltopdf website.
| import markdown | |
| import imgkit | |
| from typing import Optional, Dict | |
| import os | |
| from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE | |
| from markdown.extensions import Extension | |
| # Default styling configuration | |
| DEFAULT_FONT_FAMILY = "Arial" | |
| DEFAULT_FONT_SIZE = 14 | |
| DEFAULT_WIDTH = 800 | |
| DEFAULT_BG_COLOR = "white" | |
| DEFAULT_TEXT_COLOR = "black" | |
| class ImagePathProcessor(ImageInlineProcessor): | |
| """ | |
| A custom markdown inline processor that converts relative image paths to absolute paths. | |
| This processor extends the default image processor to handle path resolution while | |
| maintaining all the standard markdown image syntax features. | |
| """ | |
| def __init__(self, base_dir: str, *args, **kwargs): | |
| """ | |
| Initialize the processor with a base directory for resolving relative paths. | |
| Args: | |
| base_dir: The base directory to resolve relative paths against | |
| *args, **kwargs: Arguments passed to the parent ImageInlineProcessor | |
| """ | |
| self.base_dir = base_dir | |
| super().__init__(*args, **kwargs) | |
| def handleMatch(self, m, data): | |
| """ | |
| Process each image match in the markdown text. | |
| This method is called for each image found in the markdown text. It converts | |
| relative paths to absolute paths while leaving absolute paths and URLs unchanged. | |
| Args: | |
| m: The regex match object | |
| data: The source text data | |
| Returns: | |
| A tuple of (ElementTree.Element, start_index, end_index) | |
| """ | |
| el, start, end = super().handleMatch(m, data) | |
| if el is not None: | |
| src = el.get("src") | |
| if src and not src.startswith(("http://", "https://", "/")): | |
| # Convert relative path to absolute path | |
| absolute_path = os.path.abspath(os.path.join(self.base_dir, src)) | |
| el.set("src", absolute_path) | |
| return el, start, end | |
| class ImagePathExtension(Extension): | |
| """ | |
| A markdown extension that adds support for converting relative image paths to absolute paths. | |
| This extension replaces the default image processor with our custom one. | |
| """ | |
| def __init__(self, base_dir: str, **kwargs): | |
| """ | |
| Initialize the extension with a base directory for path resolution. | |
| Args: | |
| base_dir: The base directory to resolve relative paths against | |
| **kwargs: Additional arguments passed to the Extension class | |
| """ | |
| self.base_dir = base_dir | |
| super().__init__(**kwargs) | |
| def extendMarkdown(self, md): | |
| """ | |
| Add this extension to the markdown parser. | |
| This method is called by the markdown parser to integrate our custom image processor. | |
| It replaces the default image pattern with our custom one while maintaining the | |
| same priority and pattern matching. | |
| Args: | |
| md: The markdown parser instance | |
| """ | |
| # Add our custom image pattern with the same priority | |
| image_pattern = ImagePathProcessor(self.base_dir, IMAGE_LINK_RE, md) | |
| md.inlinePatterns.register(image_pattern, "image_link", 150) | |
| def get_default_css( | |
| font_family: str = DEFAULT_FONT_FAMILY, | |
| font_size: int = DEFAULT_FONT_SIZE, | |
| width: int = DEFAULT_WIDTH, | |
| background_color: str = DEFAULT_BG_COLOR, | |
| text_color: str = DEFAULT_TEXT_COLOR, | |
| ) -> str: | |
| """ | |
| Generate default CSS styling for the HTML document. | |
| The CSS includes styling for basic elements like body, headings, images, | |
| code blocks, and maintains a clean, readable layout. | |
| """ | |
| return f""" | |
| body {{ | |
| font-family: {font_family}, sans-serif; | |
| font-size: {font_size}px; | |
| color: {text_color}; | |
| background-color: {background_color}; | |
| width: {width}px; | |
| margin: 20px; | |
| line-height: 1.5; | |
| }} | |
| img {{ | |
| max-width: 100%; | |
| height: auto; | |
| }} | |
| h1 {{ font-size: 2em; margin-top: 0.67em; margin-bottom: 0.67em; }} | |
| h2 {{ font-size: 1.5em; margin-top: 0.83em; margin-bottom: 0.83em; }} | |
| h3 {{ font-size: 1.17em; margin-top: 1em; margin-bottom: 1em; }} | |
| p {{ margin-top: 1em; margin-bottom: 1em; }} | |
| code {{ | |
| background-color: #f4f4f4; | |
| padding: 2px 4px; | |
| border-radius: 4px; | |
| }} | |
| pre {{ | |
| background-color: #f4f4f4; | |
| padding: 1em; | |
| border-radius: 4px; | |
| overflow-x: auto; | |
| }} | |
| """ | |
| def create_html( | |
| markdown_text: str, | |
| base_dir: str = ".", | |
| font_family: str = DEFAULT_FONT_FAMILY, | |
| font_size: int = DEFAULT_FONT_SIZE, | |
| width: int = DEFAULT_WIDTH, | |
| background_color: str = DEFAULT_BG_COLOR, | |
| text_color: str = DEFAULT_TEXT_COLOR, | |
| ) -> str: | |
| """ | |
| Convert markdown text to styled HTML, resolving relative image paths. | |
| Args: | |
| markdown_text: Input markdown text to convert | |
| base_dir: Base directory for resolving relative image paths | |
| font_family: CSS font family to use | |
| font_size: Base font size in pixels | |
| width: Width of the output in pixels | |
| background_color: Background color of the output | |
| text_color: Color of the text | |
| Returns: | |
| Complete HTML document as a string | |
| """ | |
| # Create the image path extension with the base directory | |
| image_extension = ImagePathExtension(base_dir) | |
| # Convert markdown to HTML using GitHub-style extras and our custom extension | |
| html_content = markdown.markdown( | |
| markdown_text, | |
| extensions=["extra", "codehilite", "fenced_code", image_extension], | |
| ) | |
| # Get the CSS styling | |
| css = get_default_css( | |
| font_family=font_family, | |
| font_size=font_size, | |
| width=width, | |
| background_color=background_color, | |
| text_color=text_color, | |
| ) | |
| # Create a complete HTML document | |
| return f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <style> | |
| {css} | |
| </style> | |
| </head> | |
| <body> | |
| {html_content} | |
| </body> | |
| </html> | |
| """ | |
| def convert_markdown_to_png( | |
| markdown_text: str, | |
| output_path: str, | |
| base_dir: str = ".", | |
| font_family: str = DEFAULT_FONT_FAMILY, | |
| font_size: int = DEFAULT_FONT_SIZE, | |
| width: int = DEFAULT_WIDTH, | |
| background_color: str = DEFAULT_BG_COLOR, | |
| text_color: str = DEFAULT_TEXT_COLOR, | |
| imgkit_options: Optional[Dict] = None, | |
| ) -> None: | |
| """ | |
| Convert markdown text to a PNG image and save it to a file. | |
| Args: | |
| markdown_text: Input markdown text to convert | |
| output_path: Path where the PNG file should be saved | |
| base_dir: Base directory for resolving relative image paths | |
| font_family: CSS font family to use | |
| font_size: Base font size in pixels | |
| width: Width of the output in pixels | |
| background_color: Background color of the output | |
| text_color: Color of the text | |
| imgkit_options: Optional dictionary of additional wkhtmltoimage options | |
| """ | |
| # Set up default imgkit options | |
| default_options = { | |
| "format": "png", | |
| "encoding": "UTF-8", | |
| "quiet": "", | |
| "enable-local-file-access": "", # Enable local file access for images | |
| } | |
| # Update with any custom options | |
| if imgkit_options: | |
| default_options.update(imgkit_options) | |
| # Create the HTML content with resolved image paths | |
| html_content = create_html( | |
| markdown_text, | |
| base_dir=base_dir, | |
| font_family=font_family, | |
| font_size=font_size, | |
| width=width, | |
| background_color=background_color, | |
| text_color=text_color, | |
| ) | |
| # Convert HTML to PNG using wkhtmltoimage | |
| imgkit.from_string(html_content, output_path, options=default_options) | |
| if __name__ == "__main__": | |
| # Example usage | |
| markdown_text = """ | |
| # Sample Markdown Document | |
| This is a paragraph with **bold** and *italic* text. | |
| ## Code Example | |
| ```python | |
| def hello_world(): | |
| print("Hello, World!") | |
| ``` | |
|  | |
| ### Lists | |
| * Item 1 | |
| * Item 2 | |
| * Nested item | |
| 1. Numbered item | |
| 2. Another numbered item | |
| """.strip() | |
| # Convert markdown to PNG with custom settings and image path resolution | |
| convert_markdown_to_png( | |
| markdown_text, | |
| "output.png", | |
| base_dir=os.path.dirname( | |
| __file__ | |
| ), # Use the current directory for image resolution | |
| font_family="Helvetica", | |
| font_size=16, | |
| width=1000, | |
| imgkit_options={"quality": 100}, | |
| ) |