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 %}
Profile
{% 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)