Spaces:
Runtime error
Runtime error
""" | |
Simple LaTeX Resume Generator | |
Generates LaTeX content that matches the working sample.tex format exactly | |
""" | |
import os | |
import subprocess | |
import tempfile | |
import shutil | |
import re | |
import sys | |
from typing import Dict, Any, List, Union | |
class LatexResumeGenerator: | |
"""Generates LaTeX resumes matching the sample.tex format""" | |
def __init__(self): | |
self.template = self._get_template() | |
def _escape_latex(self, text: Union[str, List]) -> str: | |
"""Simple LaTeX escaping for normal user input""" | |
# Handle case where text might be a list (fix for the original error) | |
if isinstance(text, list): | |
text = ', '.join(str(item) for item in text) | |
elif not isinstance(text, str): | |
text = str(text) if text is not None else "" | |
if not text: | |
return "" | |
# Simple character escaping for normal text - order matters! | |
escaped_text = text | |
# Handle backslashes first to avoid double escaping | |
escaped_text = escaped_text.replace('\\', r'\textbackslash{}') | |
# Then handle other special characters | |
latex_special_chars = { | |
'{': r'\{', | |
'}': r'\}', | |
'&': r'\&', | |
'%': r'\%', | |
'$': r'\$', | |
'#': r'\#', | |
'^': r'\textasciicircum{}', | |
'_': r'\_', | |
'~': r'\textasciitilde{}' | |
} | |
for char, escape in latex_special_chars.items(): | |
escaped_text = escaped_text.replace(char, escape) | |
return escaped_text | |
def _get_template(self) -> str: | |
"""Returns minimal LaTeX template using only BasicTeX packages""" | |
return r""" | |
%------------------------- | |
% Resume in Latex | |
% Author : Shivam Sourav (adapted from Jake Gutierrez's template) | |
% License : MIT | |
%------------------------ | |
\documentclass[letterpaper,11pt]{article} | |
\usepackage{latexsym} | |
\usepackage[margin=1in]{geometry} | |
\usepackage{titlesec} | |
\usepackage[usenames,dvipsnames]{color} | |
\usepackage{verbatim} | |
\usepackage{enumitem} | |
\usepackage[hidelinks]{hyperref} | |
\usepackage{fancyhdr} | |
\pagestyle{fancy} | |
\fancyhf{} % clear all header and footer fields | |
\fancyfoot{} | |
\renewcommand{\headrulewidth}{0pt} | |
\renewcommand{\footrulewidth}{0pt} | |
% Adjust margins | |
\addtolength{\oddsidemargin}{-0.5in} | |
\addtolength{\evensidemargin}{-0.5in} | |
\addtolength{\textwidth}{1.2in} | |
\addtolength{\topmargin}{-.5in} | |
\addtolength{\textheight}{1.5in} | |
\urlstyle{same} | |
\raggedbottom | |
\raggedright | |
\setlength{\tabcolsep}{0in} | |
% Sections formatting | |
\titleformat{\section}{ | |
\vspace{-4pt}\scshape\raggedright\large | |
}{}{0em}{}[\color{black}\titlerule \vspace{-5pt}] | |
%------------------------- | |
% Custom commands | |
\newcommand{\resumeItem}[1]{ | |
\item \small{ | |
{#1 \vspace{-2pt}} | |
} | |
} | |
\newcommand{\resumeSubheading}[4]{ | |
\vspace{-2pt}\item | |
\begin{tabular*}{0.97\textwidth}[t]{l@{\extracolsep{\fill}}r} | |
\textbf{#1} & #2 \\ | |
\textit{\small#3} & \textit{\small#4} \\ | |
\end{tabular*}\vspace{-5pt} | |
} | |
\newcommand{\resumeProjectHeading}[2]{ | |
\vspace{-2pt}\item | |
\begin{tabular*}{0.97\textwidth}[t]{l@{\extracolsep{\fill}}r} | |
\small#1 & #2 \\ | |
\end{tabular*}\vspace{-5pt} | |
} | |
\newcommand{\resumeSubHeadingListStart}{\begin{itemize}[leftmargin=0.15in, label={}]} | |
\newcommand{\resumeSubHeadingListEnd}{\end{itemize}} | |
\newcommand{\resumeItemListStart}{\begin{itemize}} | |
\newcommand{\resumeItemListEnd}{\end{itemize}\vspace{-5pt}} | |
%------------------------------------------- | |
%%%%%% RESUME STARTS HERE %%%%%%%%%%%%%%%%%%%%%%%%%%%% | |
\begin{document} | |
%----------HEADING---------- | |
\begin{center} | |
\textbf{\Huge \scshape {{NAME}}} \\ \vspace{1pt} | |
{{CONTACT}} | |
\end{center} | |
%-----------EXPERIENCE----------- | |
\section{Professional Experience} | |
\resumeSubHeadingListStart | |
{{EXPERIENCE}} | |
\resumeSubHeadingListEnd | |
%-----------EDUCATION----------- | |
\section{Education} | |
\resumeSubHeadingListStart | |
{{EDUCATION}} | |
\resumeSubHeadingListEnd | |
%-----------PROJECTS----------- | |
\section{University Projects} | |
\resumeSubHeadingListStart | |
{{PROJECTS}} | |
\resumeSubHeadingListEnd | |
\section{Additional} | |
\begin{itemize} | |
{{SKILLS}} | |
\end{itemize} | |
\end{document} | |
""" | |
def generate_latex(self, data: Dict[str, Any]) -> str: | |
"""Generate LaTeX content from data""" | |
# Add debugging to see what data we receive | |
print("=== DEBUG: Received data ===", file=sys.stderr) | |
print(f"Raw data keys: {list(data.keys())}", file=sys.stderr) | |
print(f"Full data: {data}", file=sys.stderr) | |
print("=== END DEBUG ===", file=sys.stderr) | |
content = self.template | |
# Replace placeholders with escaped content | |
content = content.replace("{{NAME}}", self._escape_latex(data.get("name", ""))) | |
content = content.replace("{{CONTACT}}", self._build_contact(data)) | |
content = content.replace("{{EXPERIENCE}}", self._build_experience(data.get("experiences", []))) | |
content = content.replace("{{EDUCATION}}", self._build_education(data.get("education", []))) | |
content = content.replace("{{PROJECTS}}", self._build_projects(data.get("projects", []))) | |
content = content.replace("{{SKILLS}}", self._build_skills(data.get("skills", {}))) | |
return content | |
def _build_contact(self, data: Dict[str, Any]) -> str: | |
"""Build contact section exactly like sample.tex""" | |
email = self._escape_latex(data.get("email", "")) | |
location = self._escape_latex(data.get("location", "")) | |
linkedin_url = data.get("linkedin_url", "") | |
github_url = data.get("github_url", "") | |
print(f"DEBUG Contact - Email: '{email}', Location: '{location}', LinkedIn: '{linkedin_url}', GitHub: '{github_url}'", file=sys.stderr) | |
contact_parts = [] | |
# Email and location on first line | |
if email: | |
contact_parts.append(f"\\href{{mailto:{email}}}{{\\underline{{{email}}}}}") | |
if location: | |
contact_parts.append(location) | |
first_line = " $|$ ".join(contact_parts) | |
# LinkedIn and GitHub on second line | |
second_line_parts = [] | |
if linkedin_url: | |
clean_url = linkedin_url.replace("https://", "").replace("http://", "").replace("www.", "") | |
second_line_parts.append(f"\\href{{{linkedin_url}}}{{\\underline{{{clean_url}}}}}") | |
if github_url: | |
clean_url = github_url.replace("https://", "").replace("http://", "") | |
second_line_parts.append(f"\\href{{{github_url}}}{{\\underline{{{clean_url}}}}}") | |
if second_line_parts: | |
second_line = " $|$\n ".join(second_line_parts) | |
result = f"{first_line} \\\\ {second_line}" | |
else: | |
result = first_line | |
print(f"DEBUG Contact result: '{result}'", file=sys.stderr) | |
return result | |
def _build_experience(self, experiences: List[Dict[str, Any]]) -> str: | |
"""Build experience section with simple escaping only""" | |
print(f"DEBUG Experience - Processing {len(experiences)} experiences", file=sys.stderr) | |
if not experiences: | |
print("DEBUG Experience - No experiences found", file=sys.stderr) | |
return "" | |
sections = [] | |
for i, exp in enumerate(experiences): | |
print(f"DEBUG Experience {i}: {exp}", file=sys.stderr) | |
title = self._escape_latex(exp.get("title", "")) | |
dates = self._escape_latex(exp.get("dates", "")) | |
company = self._escape_latex(exp.get("company", "")) | |
location = self._escape_latex(exp.get("location", "")) | |
section = f""" \\resumeSubheading | |
{{{title}}}{{{dates}}} | |
{{{company}}}{{{location}}} | |
\\resumeItemListStart""" | |
# Add responsibilities with simple escaping only | |
responsibilities = exp.get("responsibilities", []) | |
print(f"DEBUG Responsibilities for exp {i}: {responsibilities}", file=sys.stderr) | |
for j, resp in enumerate(responsibilities): | |
# Handle both string and list inputs | |
if isinstance(resp, list): | |
resp = ', '.join(str(item) for item in resp) | |
elif not isinstance(resp, str): | |
resp = str(resp) if resp is not None else "" | |
if resp and resp.strip(): # Only add non-empty responsibilities | |
escaped_resp = self._escape_latex(resp.strip()) | |
section += f"\n \\resumeItem{{{escaped_resp}}}" | |
print(f"DEBUG Added responsibility {j}: {escaped_resp[:50]}...", file=sys.stderr) | |
section += "\n \\resumeItemListEnd" | |
sections.append(section) | |
result = "\n".join(sections) | |
print(f"DEBUG Experience result length: {len(result)}", file=sys.stderr) | |
return result | |
def _build_projects(self, projects: List[Dict[str, Any]]) -> str: | |
"""Build projects section with simple escaping only""" | |
print(f"DEBUG Projects - Processing {len(projects)} projects", file=sys.stderr) | |
if not projects: | |
print("DEBUG Projects - No projects found", file=sys.stderr) | |
return "" | |
sections = [] | |
for i, proj in enumerate(projects): | |
print(f"DEBUG Project {i}: {proj}", file=sys.stderr) | |
title = self._escape_latex(proj.get("title", "")) | |
section = f""" \\resumeProjectHeading | |
{{\\textbf{{{title}}}}}{{}} | |
\\resumeItemListStart""" | |
# Add descriptions with simple escaping only | |
descriptions = proj.get("descriptions", []) | |
print(f"DEBUG Descriptions for project {i}: {descriptions}", file=sys.stderr) | |
for j, desc in enumerate(descriptions): | |
# Handle both string and list inputs | |
if isinstance(desc, list): | |
desc = ', '.join(str(item) for item in desc) | |
elif not isinstance(desc, str): | |
desc = str(desc) if desc is not None else "" | |
if desc and desc.strip(): # Only add non-empty descriptions | |
escaped_desc = self._escape_latex(desc.strip()) | |
section += f"\n \\resumeItem{{{escaped_desc}}}" | |
print(f"DEBUG Added description {j}: {escaped_desc[:50]}...", file=sys.stderr) | |
section += "\n \\resumeItemListEnd" | |
sections.append(section) | |
result = "\n".join(sections) | |
print(f"DEBUG Projects result length: {len(result)}", file=sys.stderr) | |
return result | |
def _build_education(self, education: List[Dict[str, Any]]) -> str: | |
"""Build education section exactly like sample.tex""" | |
print(f"DEBUG Education - Processing {len(education)} education entries", file=sys.stderr) | |
if not education: | |
print("DEBUG Education - No education found", file=sys.stderr) | |
return "" | |
sections = [] | |
for i, edu in enumerate(education): | |
print(f"DEBUG Education {i}: {edu}", file=sys.stderr) | |
institution = self._escape_latex(edu.get("institution", "")) | |
date = self._escape_latex(edu.get("graduation_date", "")) | |
degree = self._escape_latex(edu.get("degree", "")) | |
gpa = self._escape_latex(edu.get("gpa", "")) | |
sections.append(f""" \\resumeSubheading | |
{{{institution}}}{{{date}}} | |
{{{degree}}}{{{gpa}}}""") | |
result = "\n".join(sections) | |
print(f"DEBUG Education result length: {len(result)}", file=sys.stderr) | |
return result | |
def _build_skills(self, skills: Dict[str, Any]) -> str: | |
"""Build skills section with simple escaping - handles both strings and lists""" | |
print(f"DEBUG Skills - Processing skills: {skills}", file=sys.stderr) | |
if not skills: | |
print("DEBUG Skills - No skills found", file=sys.stderr) | |
return "" | |
# Improved skill name mapping to handle various formats | |
skill_names = { | |
"languages": "Languages", | |
"language": "Languages", | |
"programming_languages": "Programming Languages", | |
"tools": "Tools", | |
"tool": "Tools", | |
"technologies": "Technologies", | |
"technology": "Technologies", | |
"frameworks": "Frameworks", | |
"framework": "Frameworks", | |
"libraries": "Libraries", | |
"library": "Libraries", | |
"frameworks_libraries": "Frameworks \\& Libraries", | |
"frameworks_&_libraries": "Frameworks \\& Libraries", | |
"data_visualization": "Data \\& Visualization", | |
"data_&_visualization": "Data \\& Visualization", | |
"databases": "Databases", | |
"database": "Databases", | |
"concepts": "Concepts", | |
"soft_skills": "Soft Skills", | |
"concepts_soft_skills": "Concepts \\& Soft Skills", | |
"operating_systems": "Operating Systems", | |
"os": "Operating Systems" | |
} | |
items = [] | |
for key, value in skills.items(): | |
print(f"DEBUG Skill category '{key}': '{value}'", file=sys.stderr) | |
if value and str(value).strip(): # Check for non-empty values | |
# Clean up the key and get proper name | |
clean_key = key.lower().replace(" ", "_").replace("-", "_") | |
name = skill_names.get(clean_key, key.replace("_", " ").replace("-", " ").title()) | |
# Handle both strings and lists properly | |
escaped_value = self._escape_latex(value) | |
items.append(f" \\item \\textbf{{{name}:}} {escaped_value}") | |
print(f"DEBUG Added skill item: {name}", file=sys.stderr) | |
result = "\n".join(items) | |
print(f"DEBUG Skills result: '{result}'", file=sys.stderr) | |
return result | |
def compile_to_pdf(self, latex_content: str, output_path: str) -> Dict[str, Any]: | |
"""Compile LaTeX to PDF""" | |
# Create temp directory | |
with tempfile.TemporaryDirectory() as temp_dir: | |
tex_file = os.path.join(temp_dir, "resume.tex") | |
pdf_file = os.path.join(temp_dir, "resume.pdf") | |
# Write LaTeX file | |
with open(tex_file, "w", encoding="utf-8") as f: | |
f.write(latex_content) | |
# Compile with pdflatex | |
try: | |
# Run twice for references | |
for _ in range(2): | |
result = subprocess.run( | |
["pdflatex", "-interaction=nonstopmode", "-output-directory", temp_dir, tex_file], | |
capture_output=True, | |
text=True | |
) | |
# Check if PDF was created | |
if os.path.exists(pdf_file): | |
# Copy to final location | |
shutil.copy2(pdf_file, output_path) | |
return { | |
"success": True, | |
"message": "PDF generated successfully", | |
"pdf_path": output_path | |
} | |
else: | |
return { | |
"success": False, | |
"message": f"PDF compilation failed: {result.stdout}", | |
"pdf_path": None | |
} | |
except Exception as e: | |
return { | |
"success": False, | |
"message": f"Error: {str(e)}", | |
"pdf_path": None | |
} | |
def generate_resume(data: Dict[str, Any], output_filename: str = "resume.pdf") -> Dict[str, Any]: | |
"""Main function to generate resume""" | |
generator = LatexResumeGenerator() | |
# Generate LaTeX | |
latex_content = generator.generate_latex(data) | |
# Compile to PDF | |
return generator.compile_to_pdf(latex_content, output_filename) |