Prak2005/markdown-pdf-convertor
3
1import os2import re3import sys4import glob5import logging6import gradio as gr7from typing import List, Dict, Any, Optional, Tuple8from bs4 import BeautifulSoup9import markdown10from markdown.extensions.tables import TableExtension11from markdown.extensions.fenced_code import FencedCodeExtension12from markdown.extensions.toc import TocExtension13from reportlab.lib.pagesizes import letter, A414from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle15from reportlab.lib.units import inch16from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image, PageBreak, Preformatted, ListFlowable, ListItem17from reportlab.lib.colors import HexColor, black, grey18from reportlab.lib import colors19from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT20import html21import base6422import requests23from PIL import Image as PilImage24import io25import tempfile26from datetime import datetime27 28# Set up logging29logging.basicConfig(30 level=logging.INFO,31 format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'32)33logger = logging.getLogger(__name__)34 35class MarkdownToPDFConverter:36 """37 Class to convert Markdown content to PDF using ReportLab.38 """39 def __init__(40 self, 41 output_path: str = "output.pdf", 42 page_size: str = "A4",43 margins: Tuple[float, float, float, float] = (0.75, 0.75, 0.75, 0.75),44 font_name: str = "Helvetica",45 base_font_size: int = 10,46 heading_scale: Dict[int, float] = None,47 include_toc: bool = True,48 code_style: str = "github"49 ):50 """51 Initialize the converter with configuration options.52 53 Args:54 output_path: Path to save the PDF55 page_size: Page size ("A4" or "letter")56 margins: Tuple of margins (left, right, top, bottom) in inches57 font_name: Base font name to use58 base_font_size: Base font size in points59 heading_scale: Dictionary of heading levels to font size multipliers60 include_toc: Whether to include a table of contents61 code_style: Style to use for code blocks62 """63 self.output_path = output_path64 self.page_size = A4 if page_size.upper() == "A4" else letter65 self.margins = margins66 self.font_name = font_name67 self.base_font_size = base_font_size68 self.heading_scale = heading_scale or {69 1: 2.0, # H1 is 2.0x base font size70 2: 1.7, # H2 is 1.7x base font size71 3: 1.4, # H3 is 1.4x base font size72 4: 1.2, # H4 is 1.2x base font size73 5: 1.1, # H5 is 1.1x base font size74 6: 1.0 # H6 is 1.0x base font size75 }76 self.include_toc = include_toc77 self.code_style = code_style78 79 # Initialize styles80 self.styles = getSampleStyleSheet()81 self._setup_styles()82 83 # Initialize document elements84 self.elements = []85 self.toc_entries = []86 87 def _setup_styles(self) -> None:88 """Set up custom paragraph styles for the document."""89 # Modify existing Normal style90 self.styles['Normal'].fontName = self.font_name91 self.styles['Normal'].fontSize = self.base_font_size92 self.styles['Normal'].leading = self.base_font_size * 1.293 self.styles['Normal'].spaceAfter = self.base_font_size * 0.894 95 # Heading styles96 for level in range(1, 7):97 size_multiplier = self.heading_scale.get(level, 1.0)98 heading_name = f'Heading{level}'99 100 # Check if the heading style already exists101 if heading_name in self.styles:102 # Modify existing style103 self.styles[heading_name].parent = self.styles['Normal']104 self.styles[heading_name].fontName = f'{self.font_name}-Bold'105 self.styles[heading_name].fontSize = int(self.base_font_size * size_multiplier)106 self.styles[heading_name].leading = int(self.base_font_size * size_multiplier * 1.2)107 self.styles[heading_name].spaceAfter = self.base_font_size108 self.styles[heading_name].spaceBefore = self.base_font_size * (1 + (0.2 * (7 - level)))109 else:110 # Create new style111 self.styles.add(112 ParagraphStyle(113 name=heading_name,114 parent=self.styles['Normal'],115 fontName=f'{self.font_name}-Bold',116 fontSize=int(self.base_font_size * size_multiplier),117 leading=int(self.base_font_size * size_multiplier * 1.2),118 spaceAfter=self.base_font_size,119 spaceBefore=self.base_font_size * (1 + (0.2 * (7 - level))),120 )121 )122 123 # Code block style124 self.styles.add(125 ParagraphStyle(126 name='CodeBlock',127 fontName='Courier',128 fontSize=self.base_font_size * 0.9,129 leading=self.base_font_size * 1.1,130 spaceAfter=self.base_font_size,131 spaceBefore=self.base_font_size,132 leftIndent=self.base_font_size,133 backgroundColor=HexColor('#EEEEEE'),134 borderWidth=0,135 borderPadding=self.base_font_size * 0.5,136 )137 )138 139 # List item style140 self.styles.add(141 ParagraphStyle(142 name='ListItem',143 parent=self.styles['Normal'],144 leftIndent=self.base_font_size * 2,145 firstLineIndent=-self.base_font_size,146 )147 )148 149 # Table of contents styles150 self.styles.add(151 ParagraphStyle(152 name='TOCHeading',153 parent=self.styles['Heading1'],154 fontSize=int(self.base_font_size * 1.5),155 spaceAfter=self.base_font_size * 1.5,156 )157 )158 159 for level in range(1, 4): # Create styles for TOC levels160 self.styles.add(161 ParagraphStyle(162 name=f'TOC{level}',163 parent=self.styles['Normal'],164 leftIndent=self.base_font_size * (level - 1) * 2,165 fontSize=self.base_font_size - (level - 1),166 leading=self.base_font_size * 1.4,167 )168 )169 170 def convert_file(self, md_file_path: str) -> None:171 """172 Convert a single markdown file to PDF.173 174 Args:175 md_file_path: Path to the markdown file176 """177 # Read markdown content178 with open(md_file_path, 'r', encoding='utf-8') as f:179 md_content = f.read()180 181 # Convert markdown to PDF182 self.convert_content(md_content)183 184 def convert_content(self, md_content: str) -> None:185 """186 Convert markdown content string to PDF.187 188 Args:189 md_content: Markdown content as a string190 """191 # Convert markdown to HTML192 html_content = self._md_to_html(md_content)193 194 # Convert HTML to ReportLab elements195 self._html_to_elements(html_content)196 197 # Generate the PDF198 self._generate_pdf()199 200 logger.info(f"PDF created at {self.output_path}")201 202 def convert_multiple_files(self, md_file_paths: List[str], 203 merge: bool = True,204 separate_toc: bool = False) -> None:205 """206 Convert multiple markdown files to PDF.207 208 Args:209 md_file_paths: List of paths to markdown files210 merge: Whether to merge all files into a single PDF211 separate_toc: Whether to include a separate TOC for each file212 """213 if merge:214 all_content = []215 216 for file_path in md_file_paths:217 logger.info(f"Processing {file_path}")218 with open(file_path, 'r', encoding='utf-8') as f:219 content = f.read()220 221 # Add file name as heading if more than one file222 if len(md_file_paths) > 1:223 file_name = os.path.splitext(os.path.basename(file_path))[0]224 content = f"# {file_name}\n\n{content}"225 226 # Add page break between files227 if all_content:228 all_content.append("\n\n<div class='page-break'></div>\n\n")229 230 all_content.append(content)231 232 combined_content = "\n".join(all_content)233 self.convert_content(combined_content)234 else:235 # Process each file separately236 for i, file_path in enumerate(md_file_paths):237 converter = MarkdownToPDFConverter(238 output_path=f"{os.path.splitext(file_path)[0]}.pdf",239 page_size=self.page_size,240 margins=self.margins,241 font_name=self.font_name,242 base_font_size=self.base_font_size,243 heading_scale=self.heading_scale,244 include_toc=separate_toc,245 code_style=self.code_style246 )247 converter.convert_file(file_path)248 249 def _md_to_html(self, md_content: str) -> str:250 """251 Convert markdown content to HTML.252 253 Args:254 md_content: Markdown content255 256 Returns:257 HTML content258 """259 # Define extensions for markdown conversion260 extensions = [261 'markdown.extensions.extra',262 'markdown.extensions.smarty',263 TableExtension(),264 FencedCodeExtension(),265 TocExtension(toc_depth=3) if self.include_toc else None266 ]267 268 # Remove None values269 extensions = [ext for ext in extensions if ext is not None]270 271 # Convert markdown to HTML272 html_content = markdown.markdown(md_content, extensions=extensions)273 return html_content274 275 def _html_to_elements(self, html_content: str) -> None:276 """277 Convert HTML content to ReportLab elements.278 279 Args:280 html_content: HTML content281 """282 soup = BeautifulSoup(html_content, 'html.parser')283 284 # Process elements285 for element in soup.children:286 if element.name:287 self._process_element(element)288 289 def _process_element(self, element: BeautifulSoup) -> None:290 """291 Process an HTML element and convert it to ReportLab elements.292 293 Args:294 element: BeautifulSoup element295 """296 if element.name in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']:297 level = int(element.name[1])298 text = element.get_text()299 300 # Add to TOC301 if self.include_toc:302 self.toc_entries.append((level, text))303 304 # Create heading paragraph305 self.elements.append(306 Paragraph(text, self.styles[f'Heading{level}'])307 )308 309 elif element.name == 'p':310 text = self._process_inline_elements(element)311 self.elements.append(312 Paragraph(text, self.styles['Normal'])313 )314 315 elif element.name == 'pre':316 code = element.get_text()317 self.elements.append(318 Preformatted(code, self.styles['CodeBlock'])319 )320 321 elif element.name == 'img':322 src = element.get('src', '')323 alt = element.get('alt', 'Image')324 325 # Handle different image sources326 if src.startswith('http'):327 # Remote image328 try:329 response = requests.get(src)330 img_data = response.content331 img_stream = io.BytesIO(img_data)332 image = Image(img_stream, width=4*inch, height=3*inch)333 334 # Try to get actual dimensions335 try:336 pil_img = PilImage.open(img_stream)337 width, height = pil_img.size338 aspect = width / height339 max_width = 6 * inch340 341 if width > max_width:342 new_width = max_width343 new_height = new_width / aspect344 image = Image(img_stream, width=new_width, height=new_height)345 except:346 pass # Use default size if image can't be processed347 348 self.elements.append(image)349 except:350 # If image can't be retrieved, add a placeholder351 self.elements.append(352 Paragraph(f"[Image: {alt}]", self.styles['Normal'])353 )354 elif src.startswith('data:image'):355 # Base64 encoded image356 try:357 # Extract base64 data358 b64_data = src.split(',')[1]359 img_data = base64.b64decode(b64_data)360 img_stream = io.BytesIO(img_data)361 image = Image(img_stream, width=4*inch, height=3*inch)362 self.elements.append(image)363 except:364 # If image can't be processed, add a placeholder365 self.elements.append(366 Paragraph(f"[Image: {alt}]", self.styles['Normal'])367 )368 else:369 # Local image370 if os.path.exists(src):371 image = Image(src, width=4*inch, height=3*inch)372 self.elements.append(image)373 else:374 # If image can't be found, add a placeholder375 self.elements.append(376 Paragraph(f"[Image: {alt}]", self.styles['Normal'])377 )378 379 elif element.name == 'ul' or element.name == 'ol':380 list_items = []381 bullet_type = 'bullet' if element.name == 'ul' else 'numbered'382 383 for item in element.find_all('li', recursive=False):384 text = self._process_inline_elements(item)385 list_items.append(386 ListItem(387 Paragraph(text, self.styles['ListItem']),388 leftIndent=20389 )390 )391 392 self.elements.append(393 ListFlowable(394 list_items,395 bulletType=bullet_type,396 start=1 if bullet_type == 'numbered' else None,397 bulletFormat='%s.' if bullet_type == 'numbered' else '%s'398 )399 )400 401 elif element.name == 'table':402 self._process_table(element)403 404 elif element.name == 'div' and 'page-break' in element.get('class', []):405 self.elements.append(PageBreak())406 407 elif element.name == 'hr':408 self.elements.append(Spacer(1, 0.25*inch))409 410 # Process children for complex elements411 elif element.name in ['div', 'blockquote', 'section', 'article']:412 for child in element.children:413 if hasattr(child, 'name') and child.name:414 self._process_element(child)415 416 def _process_inline_elements(self, element: BeautifulSoup) -> str:417 """418 Process inline HTML elements like bold, italic, etc.419 420 Args:421 element: BeautifulSoup element422 423 Returns:424 Formatted text with ReportLab markup425 """426 html_str = str(element)427 428 # Convert common HTML tags to ReportLab paragraph markup429 replacements = [430 (r'<strong>(.*?)</strong>', r'<b>\1</b>'),431 (r'<b>(.*?)</b>', r'<b>\1</b>'),432 (r'<em>(.*?)</em>', r'<i>\1</i>'),433 (r'<i>(.*?)</i>', r'<i>\1</i>'),434 (r'<code>(.*?)</code>', r'<font name="Courier">\1</font>'),435 (r'<a href="(.*?)">(.*?)</a>', r'<link href="\1">\2</link>'),436 (r'<u>(.*?)</u>', r'<u>\1</u>'),437 (r'<strike>(.*?)</strike>', r'<strike>\1</strike>'),438 (r'<del>(.*?)</del>', r'<strike>\1</strike>'),439 ]440 441 for pattern, replacement in replacements:442 html_str = re.sub(pattern, replacement, html_str, flags=re.DOTALL)443 444 # Extract text with our ReportLab markup from the modified HTML445 soup = BeautifulSoup(html_str, 'html.parser')446 return soup.get_text()447 448 def _process_table(self, table_element: BeautifulSoup) -> None:449 """450 Process an HTML table into a ReportLab Table.451 452 Args:453 table_element: BeautifulSoup table element454 """455 rows = []456 457 # Extract header row458 thead = table_element.find('thead')459 if thead:460 header_cells = []461 for th in thead.find_all(['th']):462 text = self._process_inline_elements(th)463 # Create a paragraph with bold text for headers464 header_cells.append(Paragraph(f"<b>{text}</b>", self.styles['Normal']))465 rows.append(header_cells)466 467 # Extract body rows468 tbody = table_element.find('tbody') or table_element469 for tr in tbody.find_all('tr'):470 if tr.parent.name == 'thead':471 continue # Skip header rows already processed472 473 row_cells = []474 for cell in tr.find_all(['td', 'th']):475 text = self._process_inline_elements(cell)476 if cell.name == 'th':477 # Headers are bold478 row_cells.append(Paragraph(f"<b>{text}</b>", self.styles['Normal']))479 else:480 row_cells.append(Paragraph(text, self.styles['Normal']))481 482 if row_cells: # Only add non-empty rows483 rows.append(row_cells)484 485 if rows:486 # Create table and style487 col_widths = [None] * len(rows[0]) # Auto width for columns488 table = Table(rows, colWidths=col_widths)489 490 # Add basic grid and header styling491 style = TableStyle([492 ('GRID', (0, 0), (-1, -1), 0.5, colors.Color(0.7, 0.7, 0.7)),493 ('BACKGROUND', (0, 0), (-1, 0), colors.Color(0.8, 0.8, 0.8)),494 ('TEXTCOLOR', (0, 0), (-1, 0), colors.black),495 ('ALIGN', (0, 0), (-1, 0), 'CENTER'),496 ('FONTNAME', (0, 0), (-1, 0), f'{self.font_name}-Bold'),497 ('BOTTOMPADDING', (0, 0), (-1, 0), 8),498 ('TOPPADDING', (0, 0), (-1, 0), 8),499 ('BOTTOMPADDING', (0, 1), (-1, -1), 6),500 ('TOPPADDING', (0, 1), (-1, -1), 6),501 ])502 503 table.setStyle(style)504 self.elements.append(table)505 506 # Add some space after the table507 self.elements.append(Spacer(1, 0.1*inch))508 509 def _generate_toc(self) -> None:510 """Generate a table of contents."""511 if not self.toc_entries:512 return513 514 self.elements.append(Paragraph("Table of Contents", self.styles['TOCHeading']))515 self.elements.append(Spacer(1, 0.2*inch))516 517 for level, text in self.toc_entries:518 if level <= 3: # Only include headings up to level 3519 self.elements.append(520 Paragraph(text, self.styles[f'TOC{level}'])521 )522 523 self.elements.append(PageBreak())524 525 def _generate_pdf(self) -> None:526 """Generate the PDF document."""527 # Create the document528 doc = SimpleDocTemplate(529 self.output_path,530 pagesize=self.page_size,531 leftMargin=self.margins[0]*inch,532 rightMargin=self.margins[1]*inch,533 topMargin=self.margins[2]*inch,534 bottomMargin=self.margins[3]*inch535 )536 537 # Add TOC if requested538 if self.include_toc and self.toc_entries:539 self._generate_toc()540 541 # Build the PDF542 doc.build(self.elements)543 544 545class MarkdownToPDFAgent:546 """547 AI Agent to convert Markdown files to PDF with enhanced formatting.548 """549 550 def __init__(self, llm=None):551 """552 Initialize the agent with optional LLM for content enhancement.553 554 Args:555 llm: Optional language model for content enhancement556 """557 self.llm = llm558 self.converter = MarkdownToPDFConverter()559 560 def setup_from_openai(self, api_key=None):561 """562 Setup agent with OpenAI LLM.563 564 Args:565 api_key: OpenAI API key (will use env var if not provided)566 """567 try:568 from langchain_openai import ChatOpenAI569 570 api_key = api_key or os.getenv("OPENAI_API_KEY")571 if not api_key:572 logger.warning("No OpenAI API key provided. Agent will run without LLM enhancement.")573 return False574 575 self.llm = ChatOpenAI(576 model="gpt-4",577 temperature=0.1,578 api_key=api_key579 )580 return True581 except ImportError:582 logger.warning("LangChain OpenAI package not found. Install with 'pip install langchain-openai'")583 return False584 585 def setup_from_gemini(self, api_key=None):586 """587 Setup agent with Google Gemini LLM.588 589 Args:590 api_key: Google Gemini API key (will use env var if not provided)591 """592 try:593 from langchain_google_genai import ChatGoogleGenerativeAI594 595 api_key = api_key or os.getenv("GOOGLE_API_KEY")596 if not api_key:597 logger.warning("No Google API key provided. Agent will run without LLM enhancement.")598 return False599 600 try:601 # Use the latest Gemini model version602 self.llm = ChatGoogleGenerativeAI(603 model="gemini-1.5-flash",604 temperature=0.1,605 google_api_key=api_key,606 convert_system_message_to_human=True607 )608 logger.info("Successfully set up Google Gemini LLM")609 return True610 except Exception as e:611 logger.error(f"Error setting up Google Gemini LLM: {str(e)}")612 return False613 except ImportError:614 logger.warning("LangChain Google Generative AI package not found. Install with 'pip install langchain-google-genai'")615 return False616 617 def enhance_markdown(self, content: str, instructions: str = None) -> str:618 """619 Enhance markdown content using LLM if available.620 621 Args:622 content: Original markdown content623 instructions: Specific enhancement instructions624 625 Returns:626 Enhanced markdown content627 """628 if not self.llm:629 logger.warning("No LLM available for enhancement. Returning original content.")630 return content631 632 default_instructions = """633 Enhance this markdown content while preserving its structure and meaning.634 Make the following improvements:635 1. Fix any grammar or spelling issues636 2. Improve formatting for better readability637 3. Ensure proper markdown syntax is used638 4. Add appropriate section headings if missing639 5. Keep the content factually identical to the original640 """641 642 instructions = instructions or default_instructions643 644 try:645 # Create a prompt for the LLM646 prompt = f"{instructions}\n\nOriginal content:\n\n{content}\n\nPlease provide the enhanced markdown content:"647 648 # Use the LLM directly with proper error handling649 try:650 from langchain.schema import HumanMessage651 logger.info(f"Using LLM type: {type(self.llm).__name__}")652 messages = [HumanMessage(content=prompt)]653 result = self.llm.invoke(messages).content654 logger.info("Successfully received response from LLM")655 except Exception as e:656 logger.error(f"Error invoking LLM: {str(e)}")657 return content658 659 # Clean up the result (extract just the markdown part)660 result = self._clean_agent_output(result)661 662 return result663 except Exception as e:664 logger.error(f"Error enhancing markdown: {str(e)}")665 return content # Return original content if enhancement fails666 667 def _clean_agent_output(self, output: str) -> str:668 """669 Clean up agent output to extract just the markdown content.670 671 Args:672 output: Raw agent output673 674 Returns:675 Cleaned markdown content676 """677 # Check if the output is wrapped in markdown code blocks678 md_pattern = r"```(?:markdown|md)?\s*([\s\S]*?)```"679 match = re.search(md_pattern, output)680 681 if match:682 return match.group(1).strip()683 684 # If no markdown blocks found, remove any agent commentary685 lines = output.split('\n')686 result_lines = []687 capture = False688 689 for line in lines:690 if capture or not (line.startswith("I") or line.startswith("Here") or line.startswith("The")):691 capture = True692 result_lines.append(line)693 694 return '\n'.join(result_lines)695 696 def process_file(self, input_path: str, output_path: str = None, enhance: bool = False, 697 enhancement_instructions: str = None, page_size: str = "A4") -> str:698 """699 Process a single markdown file and convert it to PDF.700 701 Args:702 input_path: Path to input markdown file703 output_path: Path for output PDF (defaults to input path with .pdf extension)704 enhance: Whether to enhance the content with LLM705 enhancement_instructions: Specific instructions for enhancement706 page_size: Page size for the PDF ("A4" or "letter")707 708 Returns:709 Path to the generated PDF710 """711 # Validate input file712 if not os.path.exists(input_path):713 logger.error(f"Input file not found: {input_path}")714 return None715 716 # Set default output path if not provided717 if not output_path:718 output_path = os.path.splitext(input_path)[0] + ".pdf"719 720 # Read markdown content721 with open(input_path, 'r', encoding='utf-8') as f:722 content = f.read()723 724 # Enhance content if requested725 if enhance and self.llm:726 logger.info(f"Enhancing content for {input_path}")727 content = self.enhance_markdown(content, enhancement_instructions)728 729 # Configure converter730 self.converter = MarkdownToPDFConverter(731 output_path=output_path,732 page_size=page_size733 )734 735 # Convert to PDF736 logger.info(f"Converting {input_path} to PDF")737 self.converter.convert_content(content)738 739 return output_path740 741 def process_directory(self, input_dir: str, output_dir: str = None, pattern: str = "*.md",742 enhance: bool = False, merge: bool = False,743 output_filename: str = "merged_document.pdf", 744 page_size: str = "A4") -> List[str]:745 """746 Process all markdown files in a directory.747 748 Args:749 input_dir: Path to input directory750 output_dir: Path to output directory (defaults to input directory)751 pattern: Glob pattern for markdown files752 enhance: Whether to enhance content with LLM753 merge: Whether to merge all files into a single PDF754 output_filename: Filename for merged PDF755 page_size: Page size for the PDF ("A4" or "letter")756 757 Returns:758 List of paths to generated PDFs759 """760 # Validate input directory761 if not os.path.isdir(input_dir):762 logger.error(f"Input directory not found: {input_dir}")763 return []764 765 # Set default output directory if not provided766 if not output_dir:767 output_dir = input_dir768 elif not os.path.exists(output_dir):769 os.makedirs(output_dir)770 771 # Get all markdown files772 md_files = glob.glob(os.path.join(input_dir, pattern))773 774 if not md_files:775 logger.warning(f"No markdown files found in {input_dir} with pattern {pattern}")776 return []777 778 # Sort files to ensure consistent ordering779 md_files.sort()780 781 if merge:782 logger.info(f"Merging {len(md_files)} markdown files into a single PDF")783 784 # Process each file for enhancement if requested785 if enhance and self.llm:786 enhanced_contents = []787 788 for md_file in md_files:789 logger.info(f"Enhancing content for {md_file}")790 with open(md_file, 'r', encoding='utf-8') as f:791 content = f.read()792 793 # Add file name as heading794 file_name = os.path.splitext(os.path.basename(md_file))[0]795 content = f"# {file_name}\n\n{content}"796 797 enhanced_content = self.enhance_markdown(content)798 enhanced_contents.append(enhanced_content)799 800 # Merge enhanced contents with page breaks801 merged_content = "\n\n<div class='page-break'></div>\n\n".join(enhanced_contents)802 803 # Convert merged content804 output_path = os.path.join(output_dir, output_filename)805 self.converter = MarkdownToPDFConverter(806 output_path=output_path,807 page_size=page_size808 )809 self.converter.convert_content(merged_content)810 811 return [output_path]812 else:813 # Merge without enhancement814 output_path = os.path.join(output_dir, output_filename)815 self.converter = MarkdownToPDFConverter(816 output_path=output_path,817 page_size=page_size818 )819 self.converter.convert_multiple_files(md_files, merge=True)820 821 return [output_path]822 else:823 # Process each file individually824 output_files = []825 826 for md_file in md_files:827 output_filename = os.path.splitext(os.path.basename(md_file))[0] + ".pdf"828 output_path = os.path.join(output_dir, output_filename)829 830 processed_file = self.process_file(831 md_file, 832 output_path, 833 enhance=enhance,834 page_size=page_size835 )836 837 if processed_file:838 output_files.append(processed_file)839 840 return output_files841 842 843# Helper functions for the Gradio interface844def load_sample():845 """Load a sample markdown document."""846 return """# Sample Markdown Document847 848## Introduction849This is a sample markdown document to demonstrate the capabilities of **MarkdownMuse**. You can use this as a starting point for your own documents.850 851## Features852- Convert markdown to PDF853- Support for tables and code blocks854- AI enhancement options855 856### Code Example857```python858def hello_world():859 print("Hello from MarkdownMuse!")860 return True861```862 863## Table Example864| Feature | Description | Status |865|---------|-------------|---------|866| Markdown Conversion | Convert MD to PDF | โ
|867| AI Enhancement | Improve content with AI | โ
|868| Custom Styling | Apply custom styles | โ
|869 870> **Note:** This is just a sample document. Feel free to modify it or create your own!871"""872 873def process_markdown(markdown_text, page_size, font_size, font_name, 874 margin_size, include_toc, use_ai, enhancement_instructions):875 """876 Process markdown text and generate a PDF.877 878 Returns:879 Path to generated PDF file880 """881 # Create a temporary file for the output882 temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".pdf")883 output_path = temp_file.name884 temp_file.close()885 886 # Initialize the agent and process the markdown887 agent = MarkdownToPDFAgent()888 889 # Configure converter890 agent.converter = MarkdownToPDFConverter(891 output_path=output_path,892 page_size=page_size,893 base_font_size=font_size,894 font_name=font_name,895 margins=(margin_size, margin_size, margin_size, margin_size),896 include_toc=include_toc897 )898 899 # Get Gemini API key from environment900 api_key = os.environ.get('GOOGLE_API_KEY')901 902 # Setup AI enhancement if requested903 enhance = False904 if use_ai and api_key:905 success = agent.setup_from_gemini(api_key)906 enhance = success907 908 try:909 # Create a temporary file for the markdown content910 with tempfile.NamedTemporaryFile(suffix='.md', delete=False) as temp_md_file:911 temp_md_path = temp_md_file.name912 temp_md_file.write(markdown_text.encode('utf-8'))913 914 # Process the file915 output_file = agent.process_file(916 temp_md_path,917 output_path,918 enhance=enhance,919 enhancement_instructions=enhancement_instructions if enhancement_instructions else None,920 page_size=page_size.lower()921 )922 923 # Remove the temporary md file924 os.unlink(temp_md_path)925 926 if output_file:927 return output_file, "โ
PDF generated successfully!"928 else:929 return None, "โ Error generating PDF. Please check your markdown syntax."930 except Exception as e:931 logger.error(f"Error processing markdown: {e}")932 return None, f"โ Error: {str(e)}"933 934 935# Check if the API key is available in the environment936has_api_key = bool(os.environ.get('GOOGLE_API_KEY'))937 938# Custom CSS for styling939custom_css = """940<style>941:root {942 --primary-color: #6366F1;943 --secondary-color: #8B5CF6;944 --accent-color: #4f46e5;945 --text-color: #1F2937;946 --light-text: #F9FAFB;947 --border-color: #E5E7EB;948 --background-color: #F3F4F6;949 --card-background: #FFFFFF;950 --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);951 --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);952 --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);953 --rounded-sm: 0.375rem;954 --rounded-md: 0.5rem;955 --rounded-lg: 0.75rem;956}957 958.header {959 background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);960 color: var(--light-text);961 padding: 2rem;962 border-radius: var(--rounded-lg);963 margin-bottom: 1.5rem;964 box-shadow: var(--shadow-lg);965 text-align: center;966 position: relative;967 overflow: hidden;968}969 970.header::before {971 content: "";972 position: absolute;973 top: 0;974 left: 0;975 right: 0;976 bottom: 0;977 background: repeating-linear-gradient(978 45deg,979 rgba(255, 255, 255, 0.05),980 rgba(255, 255, 255, 0.05) 10px,981 rgba(255, 255, 255, 0) 10px,982 rgba(255, 255, 255, 0) 20px983 );984}985 986.header h1 {987 font-size: 2.5rem;988 margin-bottom: 0.5rem;989 font-weight: 700;990 text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);991}992 993.header p {994 font-size: 1.25rem;995 opacity: 0.9;996 max-width: 700px;997 margin: 0 auto;998}999 1000.container {1001 max-width: 1200px;1002 margin: 0 auto;1003 padding: 0 1rem;1004}1005 1006.card {1007 background: var(--card-background);1008 border-radius: var(--rounded-lg);1009 padding: 1.5rem;1010 box-shadow: var(--shadow-md);1011 margin-bottom: 1.5rem;1012 border: 1px solid var(--border-color);1013 transition: transform 0.2s, box-shadow 0.2s;1014}1015 1016.card:hover {1017 transform: translateY(-2px);1018 box-shadow: var(--shadow-lg);1019}1020 1021.section-title {1022 color: var(--primary-color);1023 font-size: 1.5rem;1024 font-weight: 600;1025 margin-bottom: 1rem;1026 padding-bottom: 0.75rem;1027 border-bottom: 2px solid var(--border-color);1028}1029 1030.footer {1031 text-align: center;1032 margin-top: 2rem;1033 padding: 1.5rem;1034 background: var(--background-color);1035 border-radius: var(--rounded-lg);1036 font-size: 0.9rem;1037 color: var(--text-color);1038 box-shadow: var(--shadow-sm);1039}1040 1041.feature-icon {1042 display: inline-block;1043 background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);1044 color: white;1045 width: 2.5rem;1046 height: 2.5rem;1047 line-height: 2.5rem;1048 text-align: center;1049 border-radius: 50%;1050 margin-right: 0.75rem;1051 font-size: 1.25rem;1052 box-shadow: var(--shadow-sm);1053}1054 1055.button-row {1056 display: flex;1057 gap: 0.75rem;1058 margin: 1rem 0;1059}1060 1061.primary-btn {1062 background: linear-gradient(to right, var(--primary-color), var(--secondary-color)) !important;1063 transition: all 0.3s ease !important;1064 transform: translateY(0);1065 box-shadow: var(--shadow-md) !important;1066 font-weight: 600 !important;1067}1068 1069.primary-btn:hover {1070 transform: translateY(-2px);1071 box-shadow: var(--shadow-lg) !important;1072}1073 1074.secondary-btn {1075 background: var(--background-color) !important;1076 color: var(--text-color) !important;1077 border: 1px solid var(--border-color) !important;1078 font-weight: 500 !important;1079}1080 1081.tip-box {1082 background: rgba(99, 102, 241, 0.1);1083 border-left: 4px solid var(--primary-color);1084 padding: 1rem;1085 margin: 1rem 0;1086 border-radius: var(--rounded-sm);1087}1088 1089.tip-title {1090 color: var(--primary-color);1091 font-weight: 600;1092 margin-bottom: 0.5rem;1093}1094 1095/* Tab styling */1096.tab-active {1097 border-bottom: 3px solid var(--primary-color);1098 color: var(--primary-color);1099 font-weight: 600;1100}1101 1102/* Customize Gradio components */1103.gr-box {1104 border-radius: var(--rounded-md) !important;1105 border: 1px solid var(--border-color) !important;1106}1107 1108.gr-button {1109 border-radius: var(--rounded-md) !important;1110}1111 1112.gr-form {1113 border-radius: var(--rounded-md) !important;1114 border: 1px solid var(--border-color) !important;1115 box-shadow: var(--shadow-sm) !important;1116}1117 1118.gr-input {1119 border-radius: var(--rounded-md) !important;1120}1121 1122.gr-checkbox {1123 border-radius: var(--rounded-sm) !important;1124}1125 1126.gr-panel {1127 border-radius: var(--rounded-md) !important;1128}1129 1130.gr-accordion {1131 border-radius: var(--rounded-md) !important;1132}1133</style>1134"""1135 1136# Define the Gradio interface1137with gr.Blocks(title="MarkdownMuse", theme=gr.themes.Soft()) as demo:1138 # Header with custom styling1139 gr.HTML(custom_css + """1140 <div class="header">1141 <h1>๐ MarkdownMuse</h1>1142 <p>Transform your Markdown files into beautifully formatted PDFs with a single click. 1143 Professional-looking documents made simple.</p>1144 </div>1145 """)1146 1147 with gr.Row():1148 # Input Section1149 with gr.Column(scale=1):1150 gr.Markdown("## ๐ Input", elem_id="section-title")1151 1152 markdown_input = gr.TextArea(1153 placeholder="Enter your markdown content here...", 1154 label="Markdown Content",1155 lines=15,1156 elem_id="markdown-input"1157 )1158 1159 with gr.Row(elem_id="button-row"):1160 sample_btn = gr.Button("๐ Load Sample", size="sm", elem_classes="secondary-btn")1161 clear_btn = gr.Button("๐๏ธ Clear", size="sm", elem_classes="secondary-btn")1162 1163 with gr.Tabs():1164 with gr.TabItem("๐ PDF Settings", elem_classes="tab-item"):1165 with gr.Row():1166 with gr.Column(scale=1):1167 page_size = gr.Radio(1168 ["A4", "Letter"], 1169 label="Page Size",1170 value="A4",1171 elem_id="page-size"1172 )1173 include_toc = gr.Checkbox(1174 value=True, 1175 label="Include Table of Contents",1176 elem_id="include-toc"1177 )1178 1179 with gr.Column(scale=1):1180 font_name = gr.Dropdown(1181 ["Helvetica", "Times-Roman", "Courier"], 1182 label="Font Family",1183 value="Helvetica",1184 elem_id="font-name"1185 )1186 font_size = gr.Slider(1187 minimum=8, 1188 maximum=14, 1189 value=10, 1190 step=1, 1191 label="Base Font Size (pt)",1192 elem_id="font-size"1193 )1194 1195 margin_size = gr.Slider(1196 minimum=0.5, 1197 maximum=2.0, 1198 value=0.75, 1199 step=0.25, 1200 label="Margins (inches)",