import os
import gradio as gr
import markdown
from jinja2 import Environment, FileSystemLoader
from weasyprint import HTML, CSS, urls
from markdown.extensions.meta import MetaExtension
from datetime import datetime
class CVGenerator:
def __init__(self):
self.template_dir = os.path.abspath('templates')
self.env = Environment(loader=FileSystemLoader(self.template_dir))
os.makedirs(self.template_dir, exist_ok=True)
self._create_template()
def _create_template(self):
template_path = os.path.join(self.template_dir, 'cv_template.html')
template_content = """
1-Page-CV
{% if profile_image %}
{% endif %}
{{ left_column }}
{{ name }}
{{ title }}
{{ right_column }}
"""
with open(template_path, 'w', encoding='utf-8') as f:
f.write(template_content)
def parse_markdown(self, md_content):
# Get first two lines
lines = md_content.split('\n')
name = lines[1].strip('# ').strip() # Remove potential markdown heading
title = lines[2].strip('# ').strip() if len(lines) > 1 else ""
md = markdown.Markdown(extensions=[MetaExtension()])
html = md.convert(md_content)
sections = html.split('')
left_content = []
right_content = []
for section in sections:
if not section.strip():
continue
section = '' + section
if any(keyword in section.lower() for keyword in ['contact', 'education', 'skills', 'languages']):
left_content.append(section)
elif 'name' not in section.lower():
right_content.append(section)
return {
'left_column': '\n'.join(left_content),
'right_column': '\n'.join(right_content),
'name': name,
'title': title
}
def generate_pdf(self, md_content, profile_image_path=None, main_color="#1B3A4B", secondary_color="rgb(239, 61, 55)", base_font_size="8pt"):
"""Generate PDF with customizable colors and font size"""
content = self.parse_markdown(md_content)
if profile_image_path:
abs_image_path = os.path.abspath(profile_image_path)
if os.path.exists(abs_image_path):
content['profile_image'] = urls.path2url(abs_image_path)
else:
print(f"Warning: Profile image not found at {abs_image_path}")
content['profile_image'] = None
else:
content['profile_image'] = None
template = self.env.get_template('cv_template.html')
rendered_html = template.render(**content)
# CSS with variable replacements
css = CSS(string=f'''
@page {{
size: A4;
margin: 0;
}}
body {{
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
font-size: {base_font_size};
line-height: 1.2;
letter-spacing: 0.03em;
}}
.container {{
display: flex;
height: 297mm;
width: 210mm;
}}
.left-column {{
background-color: {main_color};
color: white;
width: 70mm;
padding: 20px;
height: 100%;
box-sizing: border-box;
border-left: 5px solid {secondary_color};
}}
.profile-image-container {{
width: 100%;
text-align: center;
margin-bottom: 20px;
}}
.profile-image {{
width: 60mm;
height: 60mm;
border-radius: 5px;
object-fit: cover;
display: block;
margin: 0 auto;
}}
.right-column {{
flex: 1;
padding: 20px;
background-color: white;
box-sizing: border-box;
}}
h1 {{
font-size: calc({base_font_size} * 3);
margin: 0 0 5px 0;
color: {main_color};
font-family: 'Playfair Display', Garamond, Times, serif;
}}
h2 {{
font-size: calc({base_font_size} * 1.75);
margin: 0 0 20px 0;
color: {main_color};
font-family: 'Playfair Display', Garamond, Times, serif;
}}
h3 {{
font-size: calc({base_font_size} * 1.5);
margin: 15px 0 10px 0;
border-bottom: 2px solid currentColor;
padding-bottom: 5px;
}}
.left-column h3 {{
color: white;
}}
.right-column h3 {{
color: {main_color};
}}
ul {{
list-style-type: none;
padding-left: 0;
margin: 5px 0;
}}
.left-column ul li {{
margin-bottom: 5px;
padding-left: 15px;
position: relative;
}}
.left-column ul li::before {{
content: "•";
position: absolute;
left: 0;
color: white;
font-size: {base_font_size};
}}
.right-column ul li {{
margin-bottom: 5px;
padding-left: 15px;
position: relative;
}}
.right-column ul li::before {{
content: "•";
position: absolute;
left: 0;
color: {main_color};
font-size: calc({base_font_size} * 1.5);
}}
.contact-info {{
margin-bottom: 15px;
}}
.work-experience {{
margin-bottom: 15px;
}}
.work-experience h4 {{
font-size: calc({base_font_size} * 1.25);
margin: 10px 0 5px 0;
color: {main_color};
}}
.timeline-item {{
margin-bottom: 15px;
}}
.date {{
color: #666;
font-size: calc({base_font_size} * 1.125);
}}
.references {{
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
font-size: calc({base_font_size} * 1.125);
}}
.reference {{
margin-bottom: 10px;
}}
p {{
margin: 5px 0;
}}
''')
current_datetime = datetime.now().strftime('%Y%m%d_%H%M')
output_filename = f"output_cv_{current_datetime}.pdf"
output_dir = os.path.abspath('outputs')
os.makedirs(output_dir, exist_ok=True)
output_path = os.path.join(output_dir, output_filename)
HTML(string=rendered_html, base_url=os.getcwd()).write_pdf(
output_path,
stylesheets=[css]
)
return output_path
def generate_cv(markdown_file, cover_image, main_color, secondary_color, base_font_size):
"""
Gradio interface function to generate CV
Args:
markdown_file (file): Markdown input file for CV content
cover_image (file): Cover image for the CV
main_color (str): Main color for the CV theme
secondary_color (str): Secondary color for the left border
base_font_size (str): Base font size for the CV
Returns:
str: Path to the generated PDF
"""
try:
size = base_font_size.replace('pt', '').strip()
float(size)
base_font_size = f"{size}pt"
except ValueError:
base_font_size = "8pt"
with open(markdown_file.name, 'r', encoding='utf-8') as file:
md_content = file.read()
generator = CVGenerator()
pdf_path = generator.generate_pdf(
md_content,
cover_image.name if cover_image else None,
main_color,
secondary_color,
base_font_size
)
return pdf_path
def create_gradio_app():
with gr.Blocks() as demo:
gr.Markdown("![App-Logo](https://www.abhishekpaliwal.fi/images/abhishek-logo-200px.png)")
gr.Markdown("# 1-Page PDF CV Generator (Create your designer CV today)")
gr.Markdown("**Step 1:** Download this markdown plain-text [CV template](https://huggingface.co/spaces/pandared/1-page-pdf-cv-maker/resolve/main/examples/example_cv.txt) and edit it.")
gr.Markdown("**Step 2:** Upload your edited markdown CV data (as .md or .txt) and cover image to generate a professional PDF.")
gr.Markdown("- If needed, create your circular profile image online: .")
gr.Markdown("- SOME EXAMPLES are provided at the end of this page. Try them first.")
gr.Markdown("- FOR HELP, contact me from this [Google Form.](https://docs.google.com/forms/d/e/1FAIpQLSfEcSszdpyCiGa76K5wwvuPM5gblbdPlRzHOU0nRm73l3NtEw/viewform)")
with gr.Row():
markdown_input = gr.File(
file_types=['.md', '.txt'],
label="Upload Markdown CV Data"
)
cover_image = gr.File(
file_types=['image'],
label="Upload Cover Image"
)
with gr.Row():
main_color = gr.ColorPicker(
label="Main Color Theme",
value="#1B3A4B"
)
secondary_color = gr.ColorPicker(
label="Secondary Color (Left Border)",
value="#EF3D37"
)
base_font_size = gr.Textbox(
label="Base Font Size (in pt)",
value="8",
placeholder="Enter a number (e.g., 8)"
)
generate_btn = gr.Button("Generate CV PDF")
output_pdf = gr.File(label="Generated CV PDF")
# Add example inputs
example_markdown = "examples/example_cv.txt"
example_image_circle = "examples/example_image_circle.png"
example_image_square = "examples/example_image_square.png"
gr.Examples(
examples=[
[example_markdown, example_image_circle, "#108dc7", "#ef8e38", "8"],
[example_markdown, example_image_circle, "#FC5C7D", "#6A82FB", "8.5"],
[example_markdown, example_image_square, "#23074d", "#cc5333", "9"],
],
inputs=[markdown_input, cover_image, main_color, secondary_color, base_font_size],
label="Try These Examples"
)
generate_btn.click(
fn=generate_cv,
inputs=[markdown_input, cover_image, main_color, secondary_color, base_font_size],
outputs=[output_pdf]
)
return demo
if __name__ == "__main__":
app = create_gradio_app()
app.launch(server_name="0.0.0.0", server_port=7860)