Spaces:
Running
Running
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 = """ | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>1-Page-CV</title> | |
</head> | |
<body> | |
<div class="container"> | |
<div class="left-column"> | |
{% if profile_image %} | |
<div class="profile-image-container"> | |
<img src="{{ profile_image }}" alt="Profile" class="profile-image"> | |
</div> | |
{% endif %} | |
{{ left_column }} | |
</div> | |
<div class="right-column"> | |
<h1>{{ name }}</h1> | |
<h2>{{ title }}</h2> | |
{{ right_column }} | |
</div> | |
</div> | |
</body> | |
</html> | |
""" | |
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('<h3>') | |
left_content = [] | |
right_content = [] | |
for section in sections: | |
if not section.strip(): | |
continue | |
section = '<h3>' + 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: <https://crop-circle.imageonline.co/>.") | |
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) |