Source code for plom.solutions.extractSolutions

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2021 Andrew Rechnitzer
# Copyright (C) 2022-2023 Colin B. Macdonald

from pathlib import Path
import sys
import tempfile

from PIL import Image

if sys.version_info < (3, 11):
    import tomli as tomllib
else:
    import tomllib
import tomlkit

from plom.scan import processFileToBitmaps
from plom.specVerifier import checkSolutionSpec
from plom.solutions import with_manager_messenger

source_path = Path("sourceVersions")
solution_path = Path("solutionImages")


def check_solution_files_present(numberOfVersions):
    print(f"Looking for solution files in directory '{source_path}'")
    for v in range(1, numberOfVersions + 1):
        filename = source_path / f"solutions{v}.pdf"
        print(f"Checking file {filename}")
        if not filename.is_file():
            return (False, f"Missing solution for version {v}")
    return (True, "All solution files present")


def glueImages(image_list, destination):
    # https://stackoverflow.com/questions/30227466/combine-several-images-horizontally-with-python
    images = [Image.open(img) for img in image_list]
    widths, heights = zip(*(img.size for img in images))
    total_width = sum(widths)
    max_height = max(heights)
    new_image = Image.new("RGB", (total_width, max_height))
    x_offset = 0
    for img in images:
        new_image.paste(img, (x_offset, 0))
        x_offset += img.size[0]

    new_image.save(destination)


def createSolutionSpec(testSpec):
    soln = {}
    soln["numberOfVersions"] = testSpec["numberOfVersions"]
    soln["numberOfPages"] = testSpec["numberOfPages"]
    soln["numberOfQuestions"] = testSpec["numberOfQuestions"]
    soln["solution"] = {}
    for q in range(1, testSpec["numberOfQuestions"] + 1):
        soln["solution"][str(q)] = {"pages": testSpec["question"][str(q)]["pages"]}
    return soln


def saveSolutionSpec(solutionSpec):
    with open("solutionSpec.toml", "w") as fh:
        tomlkit.dump(solutionSpec, fh)


def loadSolutionSpec(spec_filename):
    with open(spec_filename, "rb") as fh:
        solutionSpec = tomllib.load(fh)
    return solutionSpec


[docs]@with_manager_messenger def extractSolutionImages(solution_spec_filename=None, *, msgr): """Extract solution images from PDF files in special location. The PDF files need to be in a special place and have special names. TODO: doc better. Maybe the location could at least be a kwarg with a default value. Args: solution_spec_filename (str/pathlib.Path/None): the spec of the solution. If None, it tries to autoconstruct from the server's exam spec. Keyword Args: msgr (plom.Messenger/tuple): either a connected Messenger or a tuple appropriate for credientials. Return: bytes: the bitmap of the solution. """ testSpec = msgr.get_spec() if solution_spec_filename is None: solutionSpec = createSolutionSpec(testSpec) saveSolutionSpec(solutionSpec) elif Path(solution_spec_filename).is_file() is False: print(f"Cannot find file {solution_spec_filename}") return False else: solutionSpec = loadSolutionSpec(solution_spec_filename) valid, msg = checkSolutionSpec(testSpec, solutionSpec) if valid: print("Valid solution specification - continuing.") else: print(f"Error in solution specification = {msg}") return False success, msg = check_solution_files_present(solutionSpec["numberOfVersions"]) if success: print(msg) else: print(msg) return False with tempfile.TemporaryDirectory() as _td: tmp = Path(_td) # split sources pdf into page images for v in range(1, testSpec["numberOfVersions"] + 1): # TODO: Issue #1744: this function returns the filenames... processFileToBitmaps(source_path / f"solutions{v}.pdf", tmp) # time to combine things and save in solution_path solution_path.mkdir(exist_ok=True) for q in range(1, testSpec["numberOfQuestions"] + 1): sq = str(q) mxv = testSpec["numberOfVersions"] if testSpec["question"][sq]["select"] == "fix": mxv = 1 # only do version 1 if 'fix' for v in range(1, mxv + 1): print(f"Processing solutions for Q{q} V{v}") image_list = [ tmp / f"solutions{v}-{p:05}.png" for p in solutionSpec["solution"][sq]["pages"] ] # maybe processing made jpegs for i, f in enumerate(image_list): if not f.is_file(): if f.with_suffix(".jpg").is_file(): image_list[i] = f.with_suffix(".jpg") # check the image list - make sure they exist for fn in image_list: if not fn.is_file(): print( "Make sure structure of solution pdf matches your test pdf." ) raise RuntimeError( f"Error - could not find solution image = {fn.name}" ) destination = solution_path / f"solution.q{q}.v{v}.png" glueImages(image_list, destination) return True