| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301 |
- #ident "@(#)$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
- import os
- import glob
- import re
- from markdown_pdf import MarkdownPdf
- from markdown_pdf import Section
- def main():
- docs_dir = os.path.join(os.path.dirname(__file__), '..', 'docs')
- md_files = glob.glob(os.path.join(docs_dir, '*.md'))
-
- if not md_files:
- print("No markdown files found in docs/")
- return
-
- # Use relative paths for fonts to ensure portability of PDF generation
- root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')).replace('\\', '/')
-
- user_css = """
- * {
- color: #1a1a1a !important;
- }
- body {
- font-family: 'Helvetica', 'Arial', sans-serif !important;
- color: #1a1a1a !important;
- background-color: #ffffff !important;
- }
- h1, h2, h3, h4, h5, h6, h1 *, h2 *, h3 *, h4 *, h5 *, h6 * {
- color: #000000 !important;
- margin-top: 18px !important;
- margin-bottom: 8px !important;
- }
- pre {
- background-color: #f8f9fa !important;
- border: 1px solid #e9ecef !important;
- padding: 2px !important;
- margin-top: 8px !important;
- margin-bottom: 16px !important;
- border-radius: 3px !important;
- font-family: 'Courier New', 'Courier', monospace !important;
- font-size: 10pt !important;
- white-space: pre-wrap !important;
- word-break: break-all !important;
- }
- pre code, pre * {
- font-family: 'Courier New', 'Courier', monospace !important;
- font-size: 10pt !important;
- color: #212529 !important;
- background-color: #f8f9fa !important;
- white-space: pre-wrap !important;
- word-break: break-all !important;
- }
- code {
- font-family: 'Courier New', 'Courier', monospace !important;
- font-size: 10pt !important;
- color: #b02a37 !important;
- background-color: #f8f9fa !important;
- padding: 2px 4px !important;
- border-radius: 3px !important;
- white-space: pre-wrap !important;
- word-break: break-all !important;
- }
- a, a * {
- color: #0d6efd !important;
- }
- blockquote, blockquote * {
- color: #555555 !important;
- border-left: 4px solid #ccc !important;
- padding-left: 10px !important;
- }
- table, tr, td, th, table * {
- color: #1a1a1a !important;
- border-color: #cccccc !important;
- }
- th {
- background-color: #f2f2f2 !important;
- }
- ul, li {
- list-style-type: disc !important;
- }
- """
- for md_file in md_files:
- pdf_file = md_file.replace('.md', '.pdf')
- print(f"Converting {os.path.basename(md_file)} to PDF...")
-
- with open(md_file, 'r', encoding='utf-8') as f:
- md_content = f.read()
-
- import subprocess
- def sanitize_name(name):
- if not name:
- return "Francois Lange"
- name_lower = name.lower()
- if "fran" in name_lower or "lange" in name_lower or "lanfr" in name_lower:
- return "Francois Lange"
- return name
- def get_git_info_for_file(file_path):
- try:
- cmd = [
- "git", "log", "-1",
- "--date=format:%Y/%m/%d %H:%M:%S",
- "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
- "--", file_path
- ]
- out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
- if out:
- parts = out.split('|')
- if len(parts) == 9:
- parts[0] = sanitize_name(parts[0])
- parts[3] = sanitize_name(parts[3])
- return parts
- except Exception:
- pass
- author_name = "Francois Lange"
- try:
- author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "lanfr144@school.lu"
- except Exception:
- author_email = "lanfr144@school.lu"
- from datetime import datetime
- now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
- return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
- # Dynamic smudging of the Format placeholder for this specific file
- base_name = os.path.basename(md_file)
- info = get_git_info_for_file(md_file)
- replacement = f"$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
-
- # Replace the raw format template if present
- pattern = r'\$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$'
- md_content = re.sub(pattern, replacement, md_content)
-
- # Clean up absolute file:/// paths to relative paths
- md_content = re.sub(r'file:///.*?/docs/([a-zA-Z0-9_-]+)\.md', r'\1.pdf', md_content, flags=re.IGNORECASE)
- md_content = re.sub(r'file:///.*?/Food/([a-zA-Z0-9_.-]+)', r'../\1', md_content, flags=re.IGNORECASE)
-
- # Add a blank line after code blocks to force copy-paste blank line
- md_content = re.sub(r'(```[a-zA-Z0-9_-]*\n[\s\S]*?\n```)', r'\1\n\n', md_content)
-
- # Convert any relative markdown links in standard format [text](filename.md) or [text](./filename.md) to point to .pdf
- md_content = re.sub(r'\]\((?!http|https)([a-zA-Z0-9_./-]+)\.md(#[a-zA-Z0-9_-]+)?\)', r'](\1.pdf\2)', md_content, flags=re.IGNORECASE)
- try:
- pdf = MarkdownPdf(toc_level=2, optimize=True, plugins={"mermaid": {}})
-
- if base_name == 'project_report.md':
- print("Splitting project_report.md into Portrait/Landscape/Portrait sections...")
- parts = md_content.split('## 2. Project File Catalog & Documentation')
- if len(parts) == 2:
- portrait_part1 = parts[0]
- remaining = parts[1]
- parts2 = remaining.split('## 3. Directory Structure Map')
- if len(parts2) == 2:
- landscape_part = '## 2. Project File Catalog & Documentation' + parts2[0]
- portrait_part2 = '## 3. Directory Structure Map' + parts2[1]
-
- pdf.add_section(Section(portrait_part1, paper_size="A4", root=root_dir), user_css=user_css)
- pdf.add_section(Section(landscape_part, paper_size="A4-L", root=root_dir), user_css=user_css)
- pdf.add_section(Section(portrait_part2, paper_size="A4", root=root_dir), user_css=user_css)
- else:
- print("WARNING: Could not find Directory Structure Map heading. Defaulting to full portrait.")
- pdf.add_section(Section(md_content, paper_size="A4", root=root_dir), user_css=user_css)
- else:
- print("WARNING: Could not find Project File Catalog heading. Defaulting to full portrait.")
- pdf.add_section(Section(md_content, paper_size="A4", root=root_dir), user_css=user_css)
- else:
- pdf.add_section(Section(md_content, paper_size="A4", root=root_dir), user_css=user_css)
-
- # Post-process compiled PDF to insert header and footer
- import fitz
- doc = pdf._make_doc()
- total_pages = len(doc)
-
- # Parse title from md_content
- title_match = re.search(r'^#\s+(.+)$', md_content, re.MULTILINE)
- if title_match:
- doc_title = title_match.group(1).strip()
- doc_title = re.sub(r'[*_`#]', '', doc_title).strip()
- else:
- doc_title = os.path.basename(pdf_file).replace('.pdf', '').replace('_', ' ')
-
- am_path = os.path.join(docs_dir, 'am.png')
- bts_path = os.path.join(docs_dir, 'Bts.png')
-
- # Read real sizes for scaling to 10%
- width_am, height_am = 146.5, 24.5 # Fallback defaults
- if os.path.exists(am_path):
- try:
- pix_am = fitz.Pixmap(am_path)
- width_am = pix_am.width * 0.1
- height_am = pix_am.height * 0.1
- except Exception:
- pass
-
- width_bts, height_bts = 67.1, 36.8 # Fallback defaults
- if os.path.exists(bts_path):
- try:
- pix_bts = fitz.Pixmap(bts_path)
- width_bts = pix_bts.width * 0.1
- height_bts = pix_bts.height * 0.1
- except Exception:
- pass
-
- for page_idx in range(total_pages):
- page = doc[page_idx]
- width = page.rect.width
- height = page.rect.height
-
- # Header layout
- # Left: am.png picture (if exists) at 10% size
- if os.path.exists(am_path):
- image_rect = fitz.Rect(48, 12, 48 + width_am, 12 + height_am)
- page.insert_image(image_rect, filename=am_path)
-
- # Right: Bts.png logo (if exists) at 10% size
- if os.path.exists(bts_path):
- bts_rect = fitz.Rect(width - 48 - width_bts, 8, width - 48, 8 + height_bts)
- page.insert_image(bts_rect, filename=bts_path)
-
- # Middle: document title (centered)
- fontsize = 8
- fontname = "helv"
- m_width = fitz.get_text_length(doc_title, fontname=fontname, fontsize=fontsize)
-
- page.insert_text(
- fitz.Point((width - m_width) / 2, 26),
- doc_title,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- # Footer layout
- # Left: "lanfr144"
- # Middle: "Page X of Y"
- # Right: "DOPRO1"
- l_footer = "lanfr144"
- footer_text = f"Page {page_idx + 1} of {total_pages}"
- f_width = fitz.get_text_length(footer_text, fontname=fontname, fontsize=fontsize)
-
- page.insert_text(
- fitz.Point(48, height - 22),
- l_footer,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- page.insert_text(
- fitz.Point((width - f_width) / 2, height - 22),
- footer_text,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- r_footer = "DOPRO1"
- r_footer_width = fitz.get_text_length(r_footer, fontname=fontname, fontsize=fontsize)
- page.insert_text(
- fitz.Point(width - 48 - r_footer_width, height - 22),
- r_footer,
- fontname=fontname,
- fontsize=fontsize,
- color=(0.5, 0.5, 0.5)
- )
-
- # Deflate, clean, garbage collect and linearize the document
- # to fix Acrobat encoding and save prompts
- doc.save(pdf_file, clean=True, garbage=3, deflate=True)
- doc.close()
- pdf.temp_files.clean()
-
- print(f"Saved {os.path.basename(pdf_file)}")
-
- # Copy to workspace root if applicable
- import shutil
- if base_name == 'project_report.md':
- dest = os.path.join(root_dir, 'Project.pdf')
- try:
- if os.path.exists(dest):
- os.remove(dest)
- shutil.copy2(pdf_file, dest)
- print("Successfully updated root Project.pdf")
- except Exception as copy_err:
- print(f"WARNING: Could not copy to root Project.pdf: {copy_err}")
- elif base_name == 'retro_planning.md':
- dest = os.path.join(root_dir, 'Retro Planning.pdf')
- try:
- if os.path.exists(dest):
- os.remove(dest)
- shutil.copy2(pdf_file, dest)
- print("Successfully updated root Retro Planning.pdf")
- except Exception as copy_err:
- print(f"WARNING: Could not copy to root Retro Planning.pdf: {copy_err}")
-
- except Exception as e:
- print(f"WARNING: Could not save {os.path.basename(pdf_file)}. File might be locked or open in a viewer. Error: {e}")
- if __name__ == "__main__":
- main()
|