CoolFace
Apppublic

Prak2005/markdown-pdf-convertor

sourceHugging Faceapache-2.0updated 2y agoView on Hugging Face
3likes
app.py1341 linesDownload Raw Back to root
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)",

Showing the first 1,200 of 1341 lines. Download the file for the rest.