Move to real LaTeX rendering

master
Sébastien Miquel 2026-03-03 19:25:05 +01:00
parent 6222ba5dac
commit 2677e41b04
4 changed files with 119 additions and 6 deletions

View File

@ -219,6 +219,71 @@ def render_latex_text(text, width_px, bg_color=(255, 255, 255, 255), max_lines=N
final_img.alpha_composite(img) final_img.alpha_composite(img)
return final_img return final_img
import os
import tempfile
import subprocess
import PIL.ImageOps
def render_real_latex_text(text, width_px, bg_color=(255, 255, 255, 255), max_lines=None, fontsize=19):
dpi = 100
width_in = width_px / dpi
line_spacing = int(fontsize * 1.2)
# Use the 'standalone' class with 'varwidth' to auto-crop height while restricting width
latex_template = f"""\\documentclass[varwidth={width_in}in,margin=0.2cm]{{standalone}}
\\usepackage[utf8]{{inputenc}}
\\usepackage[T1]{{fontenc}}
\\usepackage{{lmodern}} % Enables arbitrary font scaling
\\usepackage{{amsmath, amssymb}}
%\\usepackage{{anyfontsize}} % replaces by lmodern
\\begin{{document}}
\\fontsize{{{fontsize}}}{{{line_spacing}}}\\selectfont
{text}
\\end{{document}}
"""
with tempfile.TemporaryDirectory() as temp_dir:
tex_path = os.path.join(temp_dir, 'text.tex')
pdf_path = os.path.join(temp_dir, 'text.pdf')
with open(tex_path, 'w', encoding='utf-8') as f:
f.write(latex_template)
# Compile to PDF
result = subprocess.run(
['pdflatex', '-interaction=nonstopmode', 'text.tex'],
cwd=temp_dir,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
if not os.path.exists(pdf_path):
raise RuntimeError("LaTeX compilation failed. Check your LaTeX syntax.")
# Convert PDF to grayscale (ignoring pdf2image's broken transparency)
images = convert_from_path(pdf_path, dpi=dpi)
gray_img = images[0].convert("L")
# 1. Invert grayscale to create an alpha mask (white bg = 0, black text = 255)
alpha_mask = PIL.ImageOps.invert(gray_img)
# 2. Create a transparent image with black text using the mask
text_img = Image.new("RGBA", gray_img.size, (0, 0, 0, 255))
text_img.putalpha(alpha_mask)
# 3. Create the requested background and composite the text over it
final_img = Image.new("RGBA", text_img.size, bg_color)
final_img.alpha_composite(text_img)
# (Optional) Truncate image height if max_lines is strictly enforced
if max_lines:
max_height_px = int((fontsize * 1.2 / 72.0) * dpi * max_lines) # Points to pixels
if final_img.height > max_height_px:
final_img = final_img.crop((0, 0, final_img.width, max_height_px))
return final_img
import io import io
from PIL import Image from PIL import Image
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
@ -376,7 +441,7 @@ def compose_label_image(base_img, label, result, hmin,
# Render Text # Render Text
txt_img = render_fn(fb['text'], width_px=ANNOT_WIDTH, txt_img = render_fn(fb['text'], width_px=ANNOT_WIDTH,
bg_color=(255, 200, 200, 180), max_lines=3) bg_color=(255, 200, 200, 180), max_lines=None)
# Calculate Position # Calculate Position
center_y = (target_ymin + target_ymax) / 2 center_y = (target_ymin + target_ymax) / 2
@ -458,7 +523,8 @@ def process_student(student_id, labels_data, root_dir, all_labels, overwrite):
d_notes[label] = str(score) d_notes[label] = str(score)
final_img, _ = compose_label_image(base_img, label, result, coordinates[0], final_img, _ = compose_label_image(base_img, label, result, coordinates[0],
with_empty=True) with_empty=True,
render_fn=render_real_latex_text)
# 7. Save Image # 7. Save Image
save_path = os.path.join(output_dir, f"{label}.jpg") save_path = os.path.join(output_dir, f"{label}.jpg")
final_img.save(save_path) final_img.save(save_path)

View File

@ -39,8 +39,9 @@ def draw_checkbox(draw, x, y, size=BOX_SIZE, label=None, fill="white"):
def safe_render_latex(*args, **kwargs): def safe_render_latex(*args, **kwargs):
"""Thread-safe wrapper for latex rendering.""" """Thread-safe wrapper for latex rendering."""
with LATEX_LOCK: # with LATEX_LOCK:
return annotating.render_latex_text(*args, **kwargs) # return annotating.render_latex_text(*args, **kwargs)
return annotating.render_real_latex_text(*args, **kwargs)
class CheckboxRenderer: class CheckboxRenderer:
def __init__(self, label_name): def __init__(self, label_name):

View File

@ -226,6 +226,7 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
# --- 2. Process Images (Cut notes, Regenerate, Concatenate) --- # --- 2. Process Images (Cut notes, Regenerate, Concatenate) ---
concat_list = [] concat_list = []
concat_list_F = []
d_notes = dict.fromkeys(all_labels, "") d_notes = dict.fromkeys(all_labels, "")
# Iterate over images defined in bnote.json to maintain order/geometry # Iterate over images defined in bnote.json to maintain order/geometry
@ -235,7 +236,8 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
# Update scores dict # Update scores dict
content = labels_data[label] content = labels_data[label]
d_notes[label] = str(content['result'].get('score', 0)) result = content['result']
d_notes[label] = str(result.get('score', 0))
# A. Cut Manual Notes # A. Cut Manual Notes
hmin, hmax = img_info["hmin"], img_info["hmax"] hmin, hmax = img_info["hmin"], img_info["hmax"]
@ -272,6 +274,14 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
concat_list.append(final_img) concat_list.append(final_img)
perfect_no_comment = True
if float(d_notes[label]) != 4.0:
perfect_no_comment = False
if len(result.get('feedback', [])) != 0:
perfect_no_comment = False
if not perfect_no_comment:
concat_list_F.append(final_img)
# --- 3. Save Final Outputs --- # --- 3. Save Final Outputs ---
with open(score_path, "w") as f: with open(score_path, "w") as f:
json.dump(d_notes, f, indent=4) json.dump(d_notes, f, indent=4)
@ -289,6 +299,18 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye
full_img.save(os.path.join(output_dir, "Concat.jpg")) full_img.save(os.path.join(output_dir, "Concat.jpg"))
print(f" Saved regenerated Concat.jpg") print(f" Saved regenerated Concat.jpg")
if concat_list_F:
max_w = max(i.width for i in concat_list_F)
total_h = sum(i.height for i in concat_list_F)
full_img = Image.new("RGB", (max_w, total_h), "white")
y = 0
for img in concat_list_F:
full_img.paste(img, (0, y))
y += img.height
full_img.save(os.path.join(output_dir, "Concat_F.jpg"))
print(f" Saved regenerated Concat_F.jpg")
from pathlib import Path from pathlib import Path

View File

@ -65,13 +65,15 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la
# --- 2. Process Images (Regenerate & Concatenate) --- # --- 2. Process Images (Regenerate & Concatenate) ---
concat_list = [] concat_list = []
concat_list_F = []
d_notes = dict.fromkeys(all_labels, "") d_notes = dict.fromkeys(all_labels, "")
# Iterate over all labels naturally to assemble a complete student profile # Iterate over all labels naturally to assemble a complete student profile
sorted_labels = sorted(labels_data.items(), key=lambda x: natural_key(x[0])) sorted_labels = sorted(labels_data.items(), key=lambda x: natural_key(x[0]))
for label, content in sorted_labels: for label, content in sorted_labels:
d_notes[label] = str(content['result'].get('score', 0)) result = content['result']
d_notes[label] = str(result.get('score', 0))
pdf_path = os.path.join(root_dir, f"Copie{student_id}", f"{label}.pdf") pdf_path = os.path.join(root_dir, f"Copie{student_id}", f"{label}.pdf")
if not os.path.exists(pdf_path): continue if not os.path.exists(pdf_path): continue
@ -102,6 +104,16 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la
concat_list.append(final_img) concat_list.append(final_img)
perfect_no_comment = True
if float(d_notes[label]) != 4.0:
perfect_no_comment = False
else:
if len(result.get('feedback', [])) != 0:
perfect_no_comment = False
if not perfect_no_comment:
concat_list_F.append(final_img)
# --- 3. Save Final Outputs --- # --- 3. Save Final Outputs ---
with open(score_path, "w") as f: with open(score_path, "w") as f:
json.dump(d_notes, f, indent=4) json.dump(d_notes, f, indent=4)
@ -119,6 +131,18 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la
full_img.save(os.path.join(output_dir, "Concat.jpg")) full_img.save(os.path.join(output_dir, "Concat.jpg"))
print(f" Saved regenerated Concat.jpg") print(f" Saved regenerated Concat.jpg")
if concat_list_F:
max_w = max(i.width for i in concat_list_F)
total_h = sum(i.height for i in concat_list_F)
full_img = Image.new("RGB", (max_w, total_h), "white")
y = 0
for img in concat_list_F:
full_img.paste(img, (0, y))
y += img.height
full_img.save(os.path.join(output_dir, "Concat_F.jpg"))
print(f" Saved regenerated Concat_F.jpg")
if __name__ == "__main__": if __name__ == "__main__":