generate_pdfs.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. #ident "@(#)$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  2. import os
  3. import glob
  4. import re
  5. from markdown_pdf import MarkdownPdf
  6. from markdown_pdf import Section
  7. def main():
  8. docs_dir = os.path.join(os.path.dirname(__file__), '..', 'docs')
  9. md_files = glob.glob(os.path.join(docs_dir, '*.md'))
  10. if not md_files:
  11. print("No markdown files found in docs/")
  12. return
  13. # Use relative paths for fonts to ensure portability of PDF generation
  14. root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')).replace('\\', '/')
  15. user_css = """
  16. @font-face {
  17. font-family: 'Roboto';
  18. src: url('docs/fonts/Roboto-Regular.ttf');
  19. }
  20. @font-face {
  21. font-family: 'Roboto';
  22. font-weight: bold;
  23. src: url('docs/fonts/Roboto-Bold.ttf');
  24. }
  25. @font-face {
  26. font-family: 'RobotoMono';
  27. src: url('docs/fonts/RobotoMono-Regular.ttf');
  28. }
  29. * {
  30. color: #1a1a1a !important;
  31. }
  32. body {
  33. font-family: 'Roboto', sans-serif;
  34. color: #1a1a1a !important;
  35. background-color: #ffffff !important;
  36. }
  37. h1, h2, h3, h4, h5, h6, h1 *, h2 *, h3 *, h4 *, h5 *, h6 * {
  38. color: #000000 !important;
  39. }
  40. pre {
  41. background-color: #212529 !important;
  42. border: 1px solid #343a40 !important;
  43. padding: 10px !important;
  44. border-radius: 4px !important;
  45. margin: 10px 0 !important;
  46. font-family: 'RobotoMono', monospace !important;
  47. white-space: pre-wrap !important;
  48. word-break: break-all !important;
  49. page-break-inside: avoid !important;
  50. break-inside: avoid !important;
  51. }
  52. pre code, pre * {
  53. font-family: 'RobotoMono', monospace !important;
  54. color: #f8f9fa !important;
  55. background-color: #212529 !important;
  56. white-space: pre-wrap !important;
  57. word-break: break-all !important;
  58. }
  59. code {
  60. font-family: 'RobotoMono', monospace !important;
  61. color: #b02a37 !important;
  62. background-color: #f8f9fa !important;
  63. padding: 2px 4px !important;
  64. border-radius: 3px !important;
  65. white-space: pre-wrap !important;
  66. word-break: break-all !important;
  67. }
  68. a, a * {
  69. color: #0d6efd !important;
  70. }
  71. blockquote, blockquote * {
  72. color: #555555 !important;
  73. border-left: 4px solid #ccc !important;
  74. padding-left: 10px !important;
  75. }
  76. table, tr, td, th, table * {
  77. color: #1a1a1a !important;
  78. border-color: #cccccc !important;
  79. }
  80. th {
  81. background-color: #f2f2f2 !important;
  82. }
  83. ul, li {
  84. list-style-type: disc !important;
  85. }
  86. """
  87. for md_file in md_files:
  88. pdf_file = md_file.replace('.md', '.pdf')
  89. print(f"Converting {os.path.basename(md_file)} to PDF...")
  90. with open(md_file, 'r', encoding='utf-8') as f:
  91. md_content = f.read()
  92. import subprocess
  93. def sanitize_name(name):
  94. if not name:
  95. return "Francois Lange"
  96. name_lower = name.lower()
  97. if "fran" in name_lower or "lange" in name_lower or "lanfr" in name_lower:
  98. return "Francois Lange"
  99. return name
  100. def get_git_info_for_file(file_path):
  101. try:
  102. cmd = [
  103. "git", "log", "-1",
  104. "--date=format:%Y/%m/%d %H:%M:%S",
  105. "--format=%an|%ae|%ad|%cn|%ce|%cd|%H|%D|%N",
  106. "--", file_path
  107. ]
  108. out = subprocess.check_output(cmd, stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip()
  109. if out:
  110. parts = out.split('|')
  111. if len(parts) == 9:
  112. parts[0] = sanitize_name(parts[0])
  113. parts[3] = sanitize_name(parts[3])
  114. return parts
  115. except Exception:
  116. pass
  117. author_name = "Francois Lange"
  118. try:
  119. author_email = subprocess.check_output(["git", "config", "user.email"], stderr=subprocess.DEVNULL).decode('utf-8', errors='ignore').strip() or "lanfr144@school.lu"
  120. except Exception:
  121. author_email = "lanfr144@school.lu"
  122. from datetime import datetime
  123. now_str = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
  124. return [author_name, author_email, now_str, author_name, author_email, now_str, "Not Committed Yet", "local", "none"]
  125. # Dynamic smudging of the Format placeholder for this specific file
  126. base_name = os.path.basename(md_file)
  127. info = get_git_info_for_file(md_file)
  128. replacement = f"$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$"
  129. # Replace the raw format template if present
  130. pattern = r'\$Format:LocalFoodAI_lanfr144:generate_pdfs.py:%an:%ae:%ad:%cn:%ce:%cd:%H:%D:%N$'
  131. md_content = re.sub(pattern, replacement, md_content)
  132. # Clean up absolute file:/// paths to relative paths
  133. md_content = re.sub(r'file:///.*?/docs/([a-zA-Z0-9_-]+)\.md', r'\1.pdf', md_content, flags=re.IGNORECASE)
  134. md_content = re.sub(r'file:///.*?/Food/([a-zA-Z0-9_.-]+)', r'../\1', md_content, flags=re.IGNORECASE)
  135. try:
  136. pdf = MarkdownPdf(toc_level=2, optimize=True, plugins={"mermaid": {}})
  137. if base_name == 'project_report.md':
  138. print("Splitting project_report.md into Portrait/Landscape/Portrait sections...")
  139. parts = md_content.split('## 2. Project File Catalog & Documentation')
  140. if len(parts) == 2:
  141. portrait_part1 = parts[0]
  142. remaining = parts[1]
  143. parts2 = remaining.split('## 3. Directory Structure Map')
  144. if len(parts2) == 2:
  145. landscape_part = '## 2. Project File Catalog & Documentation' + parts2[0]
  146. portrait_part2 = '## 3. Directory Structure Map' + parts2[1]
  147. pdf.add_section(Section(portrait_part1, paper_size="A4", root=root_dir), user_css=user_css)
  148. pdf.add_section(Section(landscape_part, paper_size="A4-L", root=root_dir), user_css=user_css)
  149. pdf.add_section(Section(portrait_part2, paper_size="A4", root=root_dir), user_css=user_css)
  150. else:
  151. print("WARNING: Could not find Directory Structure Map heading. Defaulting to full portrait.")
  152. pdf.add_section(Section(md_content, paper_size="A4", root=root_dir), user_css=user_css)
  153. else:
  154. print("WARNING: Could not find Project File Catalog heading. Defaulting to full portrait.")
  155. pdf.add_section(Section(md_content, paper_size="A4", root=root_dir), user_css=user_css)
  156. else:
  157. pdf.add_section(Section(md_content, paper_size="A4", root=root_dir), user_css=user_css)
  158. pdf.save(pdf_file)
  159. print(f"Saved {os.path.basename(pdf_file)}")
  160. # Copy to workspace root if applicable
  161. import shutil
  162. if base_name == 'project_report.md':
  163. dest = os.path.join(root_dir, 'Project.pdf')
  164. try:
  165. if os.path.exists(dest):
  166. os.remove(dest)
  167. shutil.copy2(pdf_file, dest)
  168. print("Successfully updated root Project.pdf")
  169. except Exception as copy_err:
  170. print(f"WARNING: Could not copy to root Project.pdf: {copy_err}")
  171. elif base_name == 'retro_planning.md':
  172. dest = os.path.join(root_dir, 'Retro Planning.pdf')
  173. try:
  174. if os.path.exists(dest):
  175. os.remove(dest)
  176. shutil.copy2(pdf_file, dest)
  177. print("Successfully updated root Retro Planning.pdf")
  178. except Exception as copy_err:
  179. print(f"WARNING: Could not copy to root Retro Planning.pdf: {copy_err}")
  180. except Exception as e:
  181. print(f"WARNING: Could not save {os.path.basename(pdf_file)}. File might be locked or open in a viewer. Error: {e}")
  182. if __name__ == "__main__":
  183. main()