from __future__ import annotations from pathlib import Path from PIL import Image from copienator import atomic_write_json, configuration, read_json, utils from copienator.filesystem import staged_directory from copienator.platform import safe_filename def publish_answer_returns(root: Path, source: Path, destination: Path) -> None: """Publish individual reviewed answers and per-question information.""" scores = read_json(source / "score.json") if not isinstance(scores, dict): raise ValueError(f"Expected a score object in {source}") info_path = source / "info.json" if not info_path.is_file(): raise ValueError(f"Missing {info_path}; recompile annotations before giving-names") info = read_json(info_path) if not isinstance(info, dict) or set(info) != set(scores): raise ValueError(f"Invalid question information in {info_path}; recompile annotations") for label, entry in info.items(): if ( not isinstance(entry, dict) or set(entry) != {"present", "not_empty", "touched", "score"} or any(type(entry[key]) is not bool for key in ("present", "not_empty", "touched")) or (entry["not_empty"] and not entry["present"]) or (entry["touched"] and not entry["not_empty"]) ): raise ValueError(f"Invalid question information in {info_path}; recompile annotations") # Match score.json, including manual score edits awaiting recompilation. entry["score"] = scores[label] answers_dir = destination / "answers" if answers_dir.is_symlink(): raise ValueError(f"Expected a real answer directory: {answers_dir}") if configuration.RETURN_ANSWERS_ENABLED: labels = [label for label, entry in info.items() if entry["present"] and entry["not_empty"]] # Import the rendering backend only when individual images are requested. from copienator.commands.annotating import make_base_image from copienator.commands.reading_annotations import concatenate all_labels = utils.read_all_labels(root) with staged_directory(answers_dir) as staging: for index, label in enumerate(sorted(labels, key=utils.natural_key), 1): paths = [] if configuration.RETURN_ANSWERS_CONTEXT: paths.extend(utils.pdf_images_of_contexts(root, label, all_labels)) if configuration.RETURN_ANSWERS_QUESTION: paths.append(utils.pdf_image_of_enonce(root, label)) if configuration.RETURN_ANSWERS_SOLUTION: paths.append(utils.pdf_image_of_solution(root, label)) images = [] for path in paths: if path: supplement, _, _ = make_base_image(path) if supplement is None: raise ValueError(f"Could not render {path}") images.append(supplement) with Image.open(source / f"{label}.jpg") as answer: images.append(answer.convert("RGB")) image = concatenate(images) # Numbering avoids collisions between sanitized label filenames. image.save(staging / f"{index:03d} - {safe_filename(label)}.jpg") elif answers_dir.exists(): # Replace the managed directory with an empty one to remove stale exports. with staged_directory(answers_dir): pass atomic_write_json(destination / "info.json", info) (destination / "touched.json").unlink(missing_ok=True)