html = f'
{html}
'
content_blocks.append(html)
# Cover image + header logo
if cover_image_path:
cover_path = Path(cover_image_path).resolve()
if not cover_path.exists():
# Try relative to script
cover_path = (Path(__file__).parent / cover_image_path).resolve()
else:
cover_path = (Path(__file__).parent / "Cover.png").resolve()
if not cover_path.exists():
print(f"⚠ Cover image not found at {cover_path}, generating PDF without image cover.")
cover_img_html = ""
else:
cover_src = f"file://{cover_path}"
cover_img_html = f'

'
print(f"✓ Cover image found: {cover_path}")
if header_image_path:
header_bg_path = Path(header_image_path).resolve()
if not header_bg_path.exists():
# Try relative to script
header_bg_path = (Path(__file__).parent / header_image_path).resolve()
else:
header_bg_path = (Path(__file__).parent / "header-bg.png").resolve()
if not header_bg_path.exists():
print(f"⚠ Header image not found at {header_bg_path}, generating header without logo.")
header_content = '""'
else:
header_bg_url = f"file://{header_bg_path}"
header_content = f'url("{header_bg_url}")'
print(f"✓ Header image found: {header_bg_path}")
# CSS - FINAL VERSION with all fixes
css = """
@page {{
size: A4;
margin: 2.5cm 2.5cm 2cm 2.5cm;
@top-left {{
content: "{project_name}";
font-size: 10pt;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
color: #103595;
font-weight: 500;
vertical-align: bottom;
padding: 0.7cm 0.0cm; /* Padding for badge effect */
border-radius: 0px; /* Rounded corners */
}}
@top-center {{
content: "";
}}
@top-right {{
content: {header_content};
vertical-align: bottom;
padding-bottom: 0.3cm;
}}
@bottom-left {{
content: "{version}";
font-size: 8pt;
color: #666;
vertical-align: top;
padding-top: 0.2cm;
}}
@bottom-right {{
content: "Page " counter(page) " of " counter(pages);
font-size: 8pt;
color: #666;
vertical-align: top;
padding-top: 0.2cm;
}}
}}
@page cover {{
size: A4;
margin: 0 0 -0.5cm 0;
background: none;
@top-left {{ content: none; }}
@top-center {{ content: none; }}
@top-right {{ content: none; }}
@bottom-left {{ content: none; }}
@bottom-center{{ content: none; }}
@bottom-right {{ content: none; }}
}}
body {{
font-family: 'Georgia', 'Times New Roman', serif;
font-size: 11pt;
line-height: 1.6;
color: #333;
margin: 0;
}}
.cover {{
page: cover;
width: 100%;
height: 100vh;
page-break-after: always;
display: flex;
align-items: center;
justify-content: center;
}}
.cover-image {{
width: 100%;
height: 100%;
object-fit: contain;
object-position: center center;
margin: 0;
padding: 0;
}}
.main {{
margin: 0 auto;
max-width: 75ch;
}}
.toc {{
page-break-after: always;
}}
/* Professional color scheme: Deep Blue for h1, Medium Blue for h2, Dark Gray for h3 */
h1, h2, h3, h4, h5, h6 {{
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-weight: 600;
line-height: 1.3;
page-break-after: avoid;
}}
h1 {{
font-size: 1.8em; /* Reduced from 2.4em */
color: #1e3a5f; /* Deep Blue */
margin-top: 2em;
margin-bottom: 0.6em;
padding-bottom: 0.3em;
border-bottom: 2px solid #2c5aa0;
page-break-before: always;
page-break-after: avoid;
letter-spacing: -0.02em;
}}
h2 {{
font-size: 1.4em; /* Reduced from 1.8em */
color: #2c5aa0; /* Medium Blue */
margin-top: 1.5em;
margin-bottom: 0.5em;
page-break-after: avoid;
letter-spacing: -0.01em;
}}
h3 {{
font-size: 1.15em; /* Reduced from 1.4em */
color: #34495e; /* Dark Gray */
margin-top: 1.2em;
margin-bottom: 0.4em;
page-break-after: avoid;
font-weight: 600;
}}
h4 {{
font-size: 1.05em;
color: #555;
margin-top: 1em;
margin-bottom: 0.3em;
page-break-after: avoid;
font-weight: 600;
}}
p {{
text-align: justify;
hyphens: auto;
margin: 0.8em 0;
}}
p, li, table, blockquote {{
orphans: 2;
widows: 2;
}}
/* Image styling */
img {{
display: block;
margin: 1.5em auto;
max-width: 100%;
height: auto;
border: 1px solid #e0e0e0;
padding: 0.5em;
background: #fafafa;
}}
.missing-image {{
display: block;
margin: 1.5em auto;
padding: 1em;
background: #fff3cd;
border: 1px solid #ffc107;
color: #856404;
text-align: center;
font-style: italic;
}}
/* Lists */
ul, ol {{
margin: 0.8em 0;
padding-left: 2em;
}}
li {{
margin: 0.3em 0;
}}
/* Code blocks */
code {{
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 0.85em;
background-color: #f5f5f5;
padding: 0.2em 0.4em;
border-radius: 3px;
word-wrap: break-word;
}}
pre {{
background-color: #f5f5f5;
border: 1px solid #ddd;
border-left: 3px solid #2c5aa0;
padding: 0.8em;
overflow-x: auto;
overflow-wrap: break-word;
word-wrap: break-word;
white-space: pre-wrap;
margin: 1em 0;
page-break-inside: avoid;
font-size: 0.85em;
line-height: 1.4;
}}
pre code {{
background: none;
padding: 0;
word-wrap: break-word;
white-space: pre-wrap;
}}
/* Tables */
table {{
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
font-size: 10pt;
table-layout: auto;
}}
th {{
background-color: #2c5aa0;
color: white;
font-weight: 600;
border: 1px solid #1e3a5f;
padding: 8px 10px;
text-align: left;
vertical-align: top;
}}
td {{
border: 1px solid #ddd;
padding: 6px 10px;
text-align: left;
vertical-align: top;
word-wrap: break-word;
white-space: normal;
}}
tr:nth-child(even) {{
background-color: #f9f9f9;
}}
/* Blockquotes */
blockquote {{
background-color: #f5f5f5;
border-left: 4px solid #2c5aa0;
padding: 0.8em 1.2em;
margin: 1em 0;
font-size: 0.95em;
font-style: italic;
}}
blockquote.note {{
background-color: #fff8dc;
border-left: 4px solid #f0ad4e;
}}
blockquote.comment {{
background-color: #e7f3ff;
border-left: 4px solid #2c5aa0;
}}
/* Links */
a {{
color: #2c5aa0;
text-decoration: none;
}}
a:hover {{
text-decoration: underline;
}}
/* Horizontal rules */
hr {{
border: none;
border-top: 1px solid #ddd;
margin: 2em 0;
}}
""".format(header_content=header_content, project_name=project_name, version=version)
combined_html = """
{cover_img}
""".format(
css=css,
cover_img=cover_img_html,
toc_html=toc_html or "",
content="".join(content_blocks),
)
output_file.parent.mkdir(parents=True, exist_ok=True)
print(f"\n{'='*60}")
print(f"Generating PDF: {output_file}")
print(f"{'='*60}")
HTML(string=combined_html, base_url=str(Path(__file__).parent)).write_pdf(str(output_file))
print(f"\n✓ PDF generated successfully!")
print(f" Output: {output_file}")
print(f" Size: {output_file.stat().st_size / 1024:.1f} KB")
# ------------------ CLI entrypoint ------------------ #
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate PDF from markdown files with optional configuration file support."
)
parser.add_argument(
"--config",
"-c",
type=Path,
help="Path to configuration file (config.txt)",
)
parser.add_argument(
"input_path",
type=Path,
nargs='?',
help="Path to ToC markdown file OR root directory containing markdown files (overrides config)",
)
parser.add_argument(
"output_file",
type=Path,
nargs='?',
help="Path to output PDF file (overrides config)",
)
args = parser.parse_args()
# Load configuration if provided
if args.config:
config = load_config(args.config)
# Use command line arguments if provided, otherwise use config
input_path = args.input_path if args.input_path else Path(config['TOC_PATH'])
output_file = args.output_file if args.output_file else Path(config['OUTPUT_FILE'])
project_name = config['PROJECT_NAME']
version = config['VERSION']
cover_image = config['COVER_IMAGE']
header_image = config['HEADER_IMAGE']
else:
# Traditional mode: require both arguments
if not args.input_path or not args.output_file:
parser.error("input_path and output_file are required when not using --config")
input_path = args.input_path
output_file = args.output_file
project_name = "Document"
version = "Version 1.0"
cover_image = ""
header_image = ""
generate_pdf(input_path, output_file, project_name, version, cover_image, header_image)