From 2677e41b045fa9d890f95119031f584862c5acde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Miquel?= Date: Tue, 3 Mar 2026 19:25:05 +0100 Subject: [PATCH] Move to real LaTeX rendering --- annotating.py | 70 +++++++++++++++++++++++++++++++++- annotating_with_checks.py | 5 ++- reading_annotations.py | 24 +++++++++++- reading_grouped_annotations.py | 26 ++++++++++++- 4 files changed, 119 insertions(+), 6 deletions(-) diff --git a/annotating.py b/annotating.py index a1fe81d..8f32034 100644 --- a/annotating.py +++ b/annotating.py @@ -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) 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 from PIL import Image import matplotlib.pyplot as plt @@ -376,7 +441,7 @@ def compose_label_image(base_img, label, result, hmin, # Render Text 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 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) 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 save_path = os.path.join(output_dir, f"{label}.jpg") final_img.save(save_path) diff --git a/annotating_with_checks.py b/annotating_with_checks.py index 952d78a..2fd2587 100644 --- a/annotating_with_checks.py +++ b/annotating_with_checks.py @@ -39,8 +39,9 @@ def draw_checkbox(draw, x, y, size=BOX_SIZE, label=None, fill="white"): def safe_render_latex(*args, **kwargs): """Thread-safe wrapper for latex rendering.""" - with LATEX_LOCK: - return annotating.render_latex_text(*args, **kwargs) + # with LATEX_LOCK: + # return annotating.render_latex_text(*args, **kwargs) + return annotating.render_real_latex_text(*args, **kwargs) class CheckboxRenderer: def __init__(self, label_name): diff --git a/reading_annotations.py b/reading_annotations.py index 11eb966..c11b29b 100644 --- a/reading_annotations.py +++ b/reading_annotations.py @@ -226,6 +226,7 @@ def apply_actions_and_regenerate(root_dir, data, student_id, actions, notes_laye # --- 2. Process Images (Cut notes, Regenerate, Concatenate) --- concat_list = [] + concat_list_F = [] d_notes = dict.fromkeys(all_labels, "") # 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 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 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) + 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 --- with open(score_path, "w") as f: 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")) 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 diff --git a/reading_grouped_annotations.py b/reading_grouped_annotations.py index 4691944..af4001f 100644 --- a/reading_grouped_annotations.py +++ b/reading_grouped_annotations.py @@ -65,13 +65,15 @@ def apply_actions_and_regenerate_grouped(root_dir, data, student_id, actions, la # --- 2. Process Images (Regenerate & Concatenate) --- concat_list = [] + concat_list_F = [] d_notes = dict.fromkeys(all_labels, "") # Iterate over all labels naturally to assemble a complete student profile sorted_labels = sorted(labels_data.items(), key=lambda x: natural_key(x[0])) 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") 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) + 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 --- with open(score_path, "w") as f: 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")) 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__":