|
|
import re
|
|
|
import json
|
|
|
import os
|
|
|
from typing import List, Dict, Any
|
|
|
|
|
|
def sanitize_for_latex(name):
|
|
|
"""Convert any character that is not alphanumeric into underscore for LaTeX compatibility."""
|
|
|
return re.sub(r'[^0-9a-zA-Z_]+', '_', name)
|
|
|
|
|
|
def initialize_beamer_document(width_cm=120, height_cm=90, theme="default"):
|
|
|
"""
|
|
|
Initialize a Beamer document with specified dimensions and theme.
|
|
|
|
|
|
Args:
|
|
|
width_cm: Width in centimeters (default 120cm for poster)
|
|
|
height_cm: Height in centimeters (default 90cm for poster)
|
|
|
theme: Beamer theme name (default, Madrid, Warsaw, etc.)
|
|
|
"""
|
|
|
code = f'''\\documentclass[aspectratio=169]{{beamer}}
|
|
|
\\usepackage[utf8]{{inputenc}}
|
|
|
\\usepackage[T1]{{fontenc}}
|
|
|
\\usepackage{{graphicx}}
|
|
|
\\usepackage{{tikz}}
|
|
|
\\usepackage{{xcolor}}
|
|
|
\\usepackage{{geometry}}
|
|
|
\\usepackage{{multicol}}
|
|
|
\\usepackage{{array}}
|
|
|
\\usepackage{{booktabs}}
|
|
|
\\usepackage{{adjustbox}}
|
|
|
|
|
|
% Set page dimensions for poster
|
|
|
\\geometry{{paperwidth={width_cm}cm, paperheight={height_cm}cm, margin=1cm}}
|
|
|
|
|
|
% Beamer theme
|
|
|
\\usetheme{{{theme}}}
|
|
|
\\usecolortheme{{default}}
|
|
|
|
|
|
% Custom colors
|
|
|
\\definecolor{{titlecolor}}{{RGB}}{{47, 85, 151}}
|
|
|
\\definecolor{{textcolor}}{{RGB}}{{0, 0, 0}}
|
|
|
\\definecolor{{bgcolor}}{{RGB}}{{255, 255, 255}}
|
|
|
|
|
|
% Remove navigation symbols
|
|
|
\\setbeamertemplate{{navigation symbols}}{{}}
|
|
|
|
|
|
% Custom title page
|
|
|
\\setbeamertemplate{{title page}}{{
|
|
|
\\begin{{center}}
|
|
|
\\vspace{{1cm}}
|
|
|
{{\\color{{titlecolor}}\\Huge\\textbf{{\\inserttitle}}}}
|
|
|
\\vspace{{0.5cm}}
|
|
|
\\Large{{\\insertauthor}}
|
|
|
\\vspace{{0.3cm}}
|
|
|
\\normalsize{{\\insertinstitute}}
|
|
|
\\end{{center}}
|
|
|
}}
|
|
|
|
|
|
% Custom frame title
|
|
|
\\setbeamertemplate{{frametitle}}{{
|
|
|
\\vspace{{0.5cm}}
|
|
|
\\begin{{flushleft}}
|
|
|
{{\\color{{titlecolor}}\\Large\\textbf{{\\insertframetitle}}}}
|
|
|
\\end{{flushleft}}
|
|
|
\\vspace{{0.3cm}}
|
|
|
}}
|
|
|
|
|
|
\\begin{{document}}
|
|
|
|
|
|
% Title frame
|
|
|
\\title{{POSTER_TITLE_PLACEHOLDER}}
|
|
|
\\author{{POSTER_AUTHOR_PLACEHOLDER}}
|
|
|
\\institute{{POSTER_INSTITUTE_PLACEHOLDER}}
|
|
|
\\date{{\\today}}
|
|
|
|
|
|
\\begin{{frame}}[plain]
|
|
|
\\titlepage
|
|
|
\\end{{frame}}
|
|
|
|
|
|
'''
|
|
|
return code
|
|
|
|
|
|
def generate_beamer_section_code(section_data: Dict[str, Any], section_index: int):
|
|
|
"""
|
|
|
兼容 Paper2Poster bullet JSON:
|
|
|
- section_data 包含 title_blocks / textbox1_blocks / textbox2_blocks
|
|
|
- 每个 *_blocks 是 list[ {bullet: bool, runs: [{text: str, ...}], ...} ]
|
|
|
"""
|
|
|
def blocks_to_lines(blocks):
|
|
|
"""把 blocks 转成 list[str],并标注是否 bullet"""
|
|
|
lines = []
|
|
|
for blk in blocks or []:
|
|
|
text = " ".join([r.get("text","") for r in blk.get("runs", [])]).strip()
|
|
|
if not text:
|
|
|
continue
|
|
|
lines.append({
|
|
|
"text": text,
|
|
|
"bullet": bool(blk.get("bullet", False))
|
|
|
})
|
|
|
return lines
|
|
|
|
|
|
|
|
|
if isinstance(section_data.get("title_blocks"), list) and section_data["title_blocks"]:
|
|
|
frame_title = " ".join([r.get("text","") for r in section_data["title_blocks"][0].get("runs", [])]).strip()
|
|
|
else:
|
|
|
frame_title = section_data.get("title_str") or "Untitled"
|
|
|
|
|
|
frame_title = frame_title.replace("{","\\{").replace("}","\\}")
|
|
|
|
|
|
code = f"\n% ===== Section {section_index} =====\n"
|
|
|
code += f"\\begin{{frame}}[t]{{{frame_title}}}\n"
|
|
|
code += " \\vspace{-0.5cm}\n"
|
|
|
|
|
|
for key in ["textbox1_blocks", "textbox2_blocks"]:
|
|
|
lines = blocks_to_lines(section_data.get(key, []))
|
|
|
if not lines:
|
|
|
continue
|
|
|
|
|
|
|
|
|
if all(l["bullet"] for l in lines):
|
|
|
code += " \\begin{itemize}\n"
|
|
|
for l in lines:
|
|
|
code += f" \\item {l['text']}\n"
|
|
|
code += " \\end{itemize}\n"
|
|
|
else:
|
|
|
for l in lines:
|
|
|
if l["bullet"]:
|
|
|
code += f" \\begin{{itemize}}\\item {l['text']}\\end{{itemize}}\n"
|
|
|
else:
|
|
|
code += f" {l['text']}\\\\\n"
|
|
|
|
|
|
code += "\\end{frame}\n\n"
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
|
|
def generate_beamer_figure_code(figure_data: Dict[str, Any], figure_index: int):
|
|
|
"""
|
|
|
Generate Beamer code for including figures.
|
|
|
|
|
|
Args:
|
|
|
figure_data: Dictionary containing figure information
|
|
|
figure_index: Index of the figure
|
|
|
"""
|
|
|
figure_name = sanitize_for_latex(figure_data.get('figure_name', f'figure_{figure_index}'))
|
|
|
figure_path = figure_data.get('figure_path', '')
|
|
|
|
|
|
|
|
|
width_cm = figure_data.get('width', 10) * 2.54
|
|
|
height_cm = figure_data.get('height', 8) * 2.54
|
|
|
|
|
|
code = f'''
|
|
|
% Figure: {figure_name}
|
|
|
\\begin{{frame}}[t]{{{figure_data.get('title', 'Figure')}}}
|
|
|
\\vspace{{-0.5cm}}
|
|
|
\\begin{{center}}
|
|
|
\\includegraphics[width={width_cm:.2f}cm, height={height_cm:.2f}cm]{{{figure_path}}}
|
|
|
\\end{{center}}
|
|
|
\\vspace{{0.3cm}}
|
|
|
\\begin{{center}}
|
|
|
\\small{{\\textbf{{{figure_data.get('caption', 'Figure Caption')}}}}}
|
|
|
\\end{{center}}
|
|
|
\\end{{frame}}
|
|
|
|
|
|
'''
|
|
|
return code
|
|
|
|
|
|
def generate_beamer_poster_code(
|
|
|
sections: List[Dict[str, Any]],
|
|
|
figures: List[Dict[str, Any]],
|
|
|
poster_info: Dict[str, str],
|
|
|
width_cm: float = 120,
|
|
|
height_cm: float = 90,
|
|
|
theme: str = "default",
|
|
|
output_path: str = "poster.tex"
|
|
|
):
|
|
|
"""
|
|
|
Generate complete Beamer poster code.
|
|
|
|
|
|
Args:
|
|
|
sections: List of section dictionaries
|
|
|
figures: List of figure dictionaries
|
|
|
poster_info: Dictionary with title, author, institute
|
|
|
width_cm: Poster width in centimeters
|
|
|
height_cm: Poster height in centimeters
|
|
|
theme: Beamer theme name
|
|
|
output_path: Output .tex file path
|
|
|
"""
|
|
|
code = initialize_beamer_document(width_cm, height_cm, theme)
|
|
|
|
|
|
|
|
|
code = code.replace('POSTER_TITLE_PLACEHOLDER', poster_info.get('title', 'Poster Title'))
|
|
|
code = code.replace('POSTER_AUTHOR_PLACEHOLDER', poster_info.get('author', 'Author Name'))
|
|
|
code = code.replace('POSTER_INSTITUTE_PLACEHOLDER', poster_info.get('institute', 'Institute Name'))
|
|
|
|
|
|
|
|
|
for i, section in enumerate(sections):
|
|
|
code += generate_beamer_section_code(section, i)
|
|
|
|
|
|
|
|
|
for i, figure in enumerate(figures):
|
|
|
code += generate_beamer_figure_code(figure, i)
|
|
|
|
|
|
|
|
|
code += '''
|
|
|
\\end{document}
|
|
|
'''
|
|
|
|
|
|
return code
|
|
|
|
|
|
def save_beamer_code(code: str, output_path: str):
|
|
|
"""Save Beamer code to file."""
|
|
|
with open(output_path, 'w', encoding='utf-8') as f:
|
|
|
f.write(code)
|
|
|
|
|
|
def compile_beamer_to_pdf(tex_path: str, output_dir: str = "."):
|
|
|
"""
|
|
|
Compile Beamer .tex file to PDF using pdflatex.
|
|
|
|
|
|
Args:
|
|
|
tex_path: Path to .tex file
|
|
|
output_dir: Output directory for PDF
|
|
|
"""
|
|
|
import subprocess
|
|
|
|
|
|
try:
|
|
|
|
|
|
result1 = subprocess.run(
|
|
|
['pdflatex', '-output-directory', output_dir, tex_path],
|
|
|
capture_output=True,
|
|
|
text=True,
|
|
|
timeout=60
|
|
|
)
|
|
|
|
|
|
result2 = subprocess.run(
|
|
|
['pdflatex', '-output-directory', output_dir, tex_path],
|
|
|
capture_output=True,
|
|
|
text=True,
|
|
|
timeout=60
|
|
|
)
|
|
|
|
|
|
if result1.returncode == 0 and result2.returncode == 0:
|
|
|
print(f"Successfully compiled {tex_path} to PDF")
|
|
|
return True
|
|
|
else:
|
|
|
print(f"Error compiling {tex_path}:")
|
|
|
print(result1.stderr)
|
|
|
print(result2.stderr)
|
|
|
return False
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
print(f"Timeout while compiling {tex_path}")
|
|
|
return False
|
|
|
except Exception as e:
|
|
|
print(f"Error compiling {tex_path}: {e}")
|
|
|
return False
|
|
|
|
|
|
|
|
|
def convert_pptx_layout_to_beamer(pptx_layout_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
"""
|
|
|
Convert PowerPoint layout data to Beamer-compatible format.
|
|
|
|
|
|
Args:
|
|
|
pptx_layout_data: Layout data from PowerPoint generation
|
|
|
"""
|
|
|
beamer_data = {
|
|
|
'sections': [],
|
|
|
'figures': [],
|
|
|
'poster_info': {
|
|
|
'title': 'Default Title',
|
|
|
'author': 'Default Author',
|
|
|
'institute': 'Default Institute'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
if 'text_arrangement' in pptx_layout_data:
|
|
|
for i, text_item in enumerate(pptx_layout_data['text_arrangement']):
|
|
|
section = {
|
|
|
'section_name': text_item.get('textbox_name', f'section_{i}'),
|
|
|
'title': text_item.get('title', f'Section {i+1}'),
|
|
|
'content': text_item.get('content', 'Content placeholder')
|
|
|
}
|
|
|
beamer_data['sections'].append(section)
|
|
|
|
|
|
|
|
|
if 'figure_arrangement' in pptx_layout_data:
|
|
|
for i, figure_item in enumerate(pptx_layout_data['figure_arrangement']):
|
|
|
figure = {
|
|
|
'figure_name': figure_item.get('figure_name', f'figure_{i}'),
|
|
|
'figure_path': figure_item.get('figure_path', ''),
|
|
|
'width': figure_item.get('width', 10),
|
|
|
'height': figure_item.get('height', 8),
|
|
|
'title': figure_item.get('title', f'Figure {i+1}'),
|
|
|
'caption': figure_item.get('caption', 'Figure caption')
|
|
|
}
|
|
|
beamer_data['figures'].append(figure)
|
|
|
|
|
|
return beamer_data
|
|
|
|
|
|
|