quizcomp.util.pdf

  1import argparse
  2import logging
  3import os
  4import random
  5import traceback
  6import typing
  7
  8import edq.util.dirent
  9import edq.util.json
 10import edq.util.time
 11
 12import quizcomp.converter.tex
 13import quizcomp.external.latex
 14import quizcomp.model.question
 15import quizcomp.model.quiz
 16
 17_logger = logging.getLogger(__name__)
 18
 19OPTIONS_FILENAME: str = 'options.json'
 20
 21def make_with_args(
 22        args: argparse.Namespace,
 23        **kwargs: typing.Any,
 24        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
 25    """
 26    Use a standard args object from set_cli_args() to make a PDF quiz.
 27    """
 28
 29    if ((args.variants < 1) or (args.variants >= quizcomp.model.quiz.DEFAULT_MAX_VARIANTS)):
 30        raise ValueError(f"Number of variants must be in [1, {quizcomp.model.quiz.DEFAULT_MAX_VARIANTS}), found {args.variants}.")
 31
 32    return make_with_path(args.path, base_out_dir = args.out_dir, seed = args.seed, num_variants = args.variants,
 33            skip_key = args.skip_key, skip_tex = args.skip_tex, skip_pdf = args.skip_pdf,
 34            **kwargs)
 35
 36def make_with_path(
 37        quiz_path: str,
 38        **kwargs: typing.Any,
 39        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
 40    """ Make a PDF given the path to a quiz JSON. """
 41
 42    if (not os.path.exists(quiz_path)):
 43        raise ValueError(f"Provided path '{quiz_path}' does not exist.")
 44
 45    if (not os.path.isfile(quiz_path)):
 46        raise ValueError(f"Provided path '{quiz_path}' is not a file.")
 47
 48    quiz = quizcomp.model.quiz.Quiz.from_path(quiz_path)
 49    return make(quiz, quiz_path = quiz_path, **kwargs)
 50
 51def make_from_question_with_args(
 52        args: argparse.Namespace,
 53        **kwargs: typing.Any,
 54        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
 55    """
 56    Use a standard args object to make a PDF from a single question.
 57    """
 58
 59    if ((args.variants < 1) or (args.variants >= quizcomp.model.quiz.DEFAULT_MAX_VARIANTS)):
 60        raise ValueError(f"Number of variants must be in [1, {quizcomp.model.quiz.DEFAULT_MAX_VARIANTS}), found {args.variants}.")
 61
 62    return make_from_question_with_path(args.path,
 63            base_out_dir = args.out_dir, seed = args.seed, num_variants = args.variants,
 64            skip_key = args.skip_key, skip_tex = args.skip_tex, skip_pdf = args.skip_pdf,
 65            **kwargs)
 66
 67def make_from_question_with_path(
 68        question_path: str,
 69        **kwargs: typing.Any,
 70        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
 71    """ Make a PDF given the path to a question JSON. """
 72
 73    if (not os.path.exists(question_path)):
 74        raise ValueError(f"Provided path '{question_path}' does not exist.")
 75
 76    if (not os.path.isfile(question_path)):
 77        raise ValueError(f"Provided path '{question_path}' is not a file.")
 78
 79    question = quizcomp.model.question.Question.from_path(question_path)
 80    return make_from_question(question, **kwargs)
 81
 82def make_from_question(
 83        question: quizcomp.model.question.Question,
 84        **kwargs: typing.Any,
 85        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
 86    """ Make a PDF given a question. """
 87
 88    quiz = quizcomp.model.quiz.Variant.get_dummy(question)
 89    return make(quiz, **kwargs)
 90
 91def make(
 92        quiz: quizcomp.model.quiz.Quiz,
 93        quiz_path: typing.Union[str, None] = None,
 94        base_out_dir: typing.Union[str, None] = None,
 95        seed: typing.Union[int, None] = None,
 96        num_variants: int = 1,
 97        write_options: bool = True,
 98        skip_key: bool = False,
 99        skip_tex: bool = False,
100        skip_pdf: bool = False,
101        **kwargs: typing.Any,
102        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
103    """ Make a PDF given a quiz. """
104
105    if (base_out_dir is None):
106        base_out_dir = edq.util.dirent.get_temp_path(prefix = 'quizcomp_pdf_', rm = False)
107
108    out_dir = os.path.join(base_out_dir, quiz.get_name())
109    edq.util.dirent.mkdir(out_dir)
110
111    _logger.info("Writing TeX/PDF quiz ('%s') to '%s'.", quiz.get_name(), out_dir)
112
113    if (seed is None):
114        seed = random.randint(0, 2**64)
115
116    now = edq.util.time.Timestamp.now()
117
118    options = {
119        'create_timestamp': now,
120        'create_time': now.pretty(),
121        'seed': seed,
122        'out_dir': out_dir,
123        'quiz': {
124            'path': quiz_path,
125            'name': quiz.get_name(),
126            'version': quiz.version,
127        },
128    }
129
130    # Options for each variant.
131    variant_options = []
132
133    _logger.debug("Using seed %d.", seed)
134
135    variants = quiz.create_variants(count = num_variants, seed = seed)
136
137    for (i, variant) in enumerate(variants):
138        variant_out_dir = make_pdf(variant, out_dir = out_dir, is_key = False, skip_tex = skip_tex, skip_pdf = skip_pdf)
139
140        name = variant.get_name()
141
142        has_key = False
143        if (not skip_key):
144            try:
145                variant.name = f"{name} -- Answer Key"
146                make_pdf(variant, out_dir = out_dir, is_key = True, skip_tex = skip_tex, skip_pdf = skip_pdf)
147                has_key = True
148            except Exception:
149                _logger.warning("Failed to generate answer key for '%s'.", name)
150                _logger.debug(traceback.format_exc())
151            finally:
152                variant.name = name
153
154        variant_options.append({
155            'id': variant.variant_id,
156            'name': name,
157            'variant_index': i,
158            'seed': seed,
159            'has_key': has_key,
160            'out_dir': variant_out_dir,
161        })
162
163        _logger.info("Completed variant: '%s'.", name)
164
165    options['variants'] = variant_options
166
167    if (write_options):
168        path = os.path.join(out_dir, OPTIONS_FILENAME)
169        edq.util.json.dump_path(options, path, indent = 4)
170
171    return (quiz, variants, options)
172
173def make_pdf(
174        variant: quizcomp.model.quiz.Variant,
175        out_dir: typing.Union[str, None] = None,
176        is_key: bool = False,
177        skip_tex: bool = False,
178        skip_pdf: bool = False,
179        ) -> str:
180    """ Make a PDF given a quiz variant. """
181
182    if (out_dir is None):
183        out_dir = edq.util.dirent.get_temp_path(prefix = 'quizcomp_pdf_', rm = False)
184
185    if (variant.variant_id is not None):
186        out_dir = os.path.join(out_dir, f"Variant - {variant.variant_id}")
187
188    edq.util.dirent.mkdir(out_dir)
189
190    out_json_path = os.path.join(out_dir, f"{variant.get_name()}.json")
191    variant.to_path(out_json_path)
192
193    image_dir = os.path.join(out_dir, 'images')
194
195    out_path = os.path.join(out_dir, f"{variant.get_name()}.tex")
196
197    if (not skip_tex):
198        converter = quizcomp.converter.tex.TexTemplateConverter(
199            answer_key = is_key,
200            image_base_dir = image_dir,
201            cleanup_images = True,
202        )
203        content = converter.convert_variant(variant)
204
205        edq.util.dirent.write_file(out_path, content)
206
207    if (not skip_pdf):
208        quizcomp.external.latex.compile(out_path)
209
210    return out_dir
211
212def modify_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
213    """ Add PDF-based arguments to the CLI argument parser. """
214
215    parser.add_argument('path', metavar = 'PATH',
216        type = str,
217        help = 'The path to a JSON file.')
218
219    parser.add_argument('--out-dir', dest = 'out_dir',
220        action = 'store', type = str, default = '.',
221        help = 'The directory to put the output (default: %(default)s).')
222
223    parser.add_argument('--skip-tex', dest = 'skip_tex',
224        action = 'store_true', default = False,
225        help = 'Skip creating TeX files (default: %(default)s).')
226
227    parser.add_argument('--skip-pdf', dest = 'skip_pdf',
228        action = 'store_true', default = False,
229        help = 'Skip compiling PDFs from TeX (default: %(default)s).')
230
231    parser.add_argument('--variants', dest = 'variants',
232        action = 'store', type = int, default = 1,
233        help = 'The number of quiz variants to create (default: %(default)s).')
234
235    parser.add_argument('--skip-key', dest = 'skip_key',
236        action = 'store_true', default = False,
237        help = 'Skip creating the answer key (default: %(default)s).')
238
239    parser.add_argument('--seed', dest = 'seed',
240        action = 'store', type = int, default = None,
241        help = 'The random seed to use (defaults to a random seed).')
242
243    return parser
OPTIONS_FILENAME: str = 'options.json'
def make_with_args( args: argparse.Namespace, **kwargs: Any) -> Tuple[quizcomp.model.quiz.Quiz, List[quizcomp.model.quiz.Variant], Dict[str, Any]]:
22def make_with_args(
23        args: argparse.Namespace,
24        **kwargs: typing.Any,
25        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
26    """
27    Use a standard args object from set_cli_args() to make a PDF quiz.
28    """
29
30    if ((args.variants < 1) or (args.variants >= quizcomp.model.quiz.DEFAULT_MAX_VARIANTS)):
31        raise ValueError(f"Number of variants must be in [1, {quizcomp.model.quiz.DEFAULT_MAX_VARIANTS}), found {args.variants}.")
32
33    return make_with_path(args.path, base_out_dir = args.out_dir, seed = args.seed, num_variants = args.variants,
34            skip_key = args.skip_key, skip_tex = args.skip_tex, skip_pdf = args.skip_pdf,
35            **kwargs)

Use a standard args object from set_cli_args() to make a PDF quiz.

def make_with_path( quiz_path: str, **kwargs: Any) -> Tuple[quizcomp.model.quiz.Quiz, List[quizcomp.model.quiz.Variant], Dict[str, Any]]:
37def make_with_path(
38        quiz_path: str,
39        **kwargs: typing.Any,
40        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
41    """ Make a PDF given the path to a quiz JSON. """
42
43    if (not os.path.exists(quiz_path)):
44        raise ValueError(f"Provided path '{quiz_path}' does not exist.")
45
46    if (not os.path.isfile(quiz_path)):
47        raise ValueError(f"Provided path '{quiz_path}' is not a file.")
48
49    quiz = quizcomp.model.quiz.Quiz.from_path(quiz_path)
50    return make(quiz, quiz_path = quiz_path, **kwargs)

Make a PDF given the path to a quiz JSON.

def make_from_question_with_args( args: argparse.Namespace, **kwargs: Any) -> Tuple[quizcomp.model.quiz.Quiz, List[quizcomp.model.quiz.Variant], Dict[str, Any]]:
52def make_from_question_with_args(
53        args: argparse.Namespace,
54        **kwargs: typing.Any,
55        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
56    """
57    Use a standard args object to make a PDF from a single question.
58    """
59
60    if ((args.variants < 1) or (args.variants >= quizcomp.model.quiz.DEFAULT_MAX_VARIANTS)):
61        raise ValueError(f"Number of variants must be in [1, {quizcomp.model.quiz.DEFAULT_MAX_VARIANTS}), found {args.variants}.")
62
63    return make_from_question_with_path(args.path,
64            base_out_dir = args.out_dir, seed = args.seed, num_variants = args.variants,
65            skip_key = args.skip_key, skip_tex = args.skip_tex, skip_pdf = args.skip_pdf,
66            **kwargs)

Use a standard args object to make a PDF from a single question.

def make_from_question_with_path( question_path: str, **kwargs: Any) -> Tuple[quizcomp.model.quiz.Quiz, List[quizcomp.model.quiz.Variant], Dict[str, Any]]:
68def make_from_question_with_path(
69        question_path: str,
70        **kwargs: typing.Any,
71        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
72    """ Make a PDF given the path to a question JSON. """
73
74    if (not os.path.exists(question_path)):
75        raise ValueError(f"Provided path '{question_path}' does not exist.")
76
77    if (not os.path.isfile(question_path)):
78        raise ValueError(f"Provided path '{question_path}' is not a file.")
79
80    question = quizcomp.model.question.Question.from_path(question_path)
81    return make_from_question(question, **kwargs)

Make a PDF given the path to a question JSON.

def make_from_question( question: quizcomp.model.question.Question, **kwargs: Any) -> Tuple[quizcomp.model.quiz.Quiz, List[quizcomp.model.quiz.Variant], Dict[str, Any]]:
83def make_from_question(
84        question: quizcomp.model.question.Question,
85        **kwargs: typing.Any,
86        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
87    """ Make a PDF given a question. """
88
89    quiz = quizcomp.model.quiz.Variant.get_dummy(question)
90    return make(quiz, **kwargs)

Make a PDF given a question.

def make( quiz: quizcomp.model.quiz.Quiz, quiz_path: Optional[str] = None, base_out_dir: Optional[str] = None, seed: Optional[int] = None, num_variants: int = 1, write_options: bool = True, skip_key: bool = False, skip_tex: bool = False, skip_pdf: bool = False, **kwargs: Any) -> Tuple[quizcomp.model.quiz.Quiz, List[quizcomp.model.quiz.Variant], Dict[str, Any]]:
 92def make(
 93        quiz: quizcomp.model.quiz.Quiz,
 94        quiz_path: typing.Union[str, None] = None,
 95        base_out_dir: typing.Union[str, None] = None,
 96        seed: typing.Union[int, None] = None,
 97        num_variants: int = 1,
 98        write_options: bool = True,
 99        skip_key: bool = False,
100        skip_tex: bool = False,
101        skip_pdf: bool = False,
102        **kwargs: typing.Any,
103        ) -> typing.Tuple[quizcomp.model.quiz.Quiz, typing.List[quizcomp.model.quiz.Variant], typing.Dict[str, typing.Any]]:
104    """ Make a PDF given a quiz. """
105
106    if (base_out_dir is None):
107        base_out_dir = edq.util.dirent.get_temp_path(prefix = 'quizcomp_pdf_', rm = False)
108
109    out_dir = os.path.join(base_out_dir, quiz.get_name())
110    edq.util.dirent.mkdir(out_dir)
111
112    _logger.info("Writing TeX/PDF quiz ('%s') to '%s'.", quiz.get_name(), out_dir)
113
114    if (seed is None):
115        seed = random.randint(0, 2**64)
116
117    now = edq.util.time.Timestamp.now()
118
119    options = {
120        'create_timestamp': now,
121        'create_time': now.pretty(),
122        'seed': seed,
123        'out_dir': out_dir,
124        'quiz': {
125            'path': quiz_path,
126            'name': quiz.get_name(),
127            'version': quiz.version,
128        },
129    }
130
131    # Options for each variant.
132    variant_options = []
133
134    _logger.debug("Using seed %d.", seed)
135
136    variants = quiz.create_variants(count = num_variants, seed = seed)
137
138    for (i, variant) in enumerate(variants):
139        variant_out_dir = make_pdf(variant, out_dir = out_dir, is_key = False, skip_tex = skip_tex, skip_pdf = skip_pdf)
140
141        name = variant.get_name()
142
143        has_key = False
144        if (not skip_key):
145            try:
146                variant.name = f"{name} -- Answer Key"
147                make_pdf(variant, out_dir = out_dir, is_key = True, skip_tex = skip_tex, skip_pdf = skip_pdf)
148                has_key = True
149            except Exception:
150                _logger.warning("Failed to generate answer key for '%s'.", name)
151                _logger.debug(traceback.format_exc())
152            finally:
153                variant.name = name
154
155        variant_options.append({
156            'id': variant.variant_id,
157            'name': name,
158            'variant_index': i,
159            'seed': seed,
160            'has_key': has_key,
161            'out_dir': variant_out_dir,
162        })
163
164        _logger.info("Completed variant: '%s'.", name)
165
166    options['variants'] = variant_options
167
168    if (write_options):
169        path = os.path.join(out_dir, OPTIONS_FILENAME)
170        edq.util.json.dump_path(options, path, indent = 4)
171
172    return (quiz, variants, options)

Make a PDF given a quiz.

def make_pdf( variant: quizcomp.model.quiz.Variant, out_dir: Optional[str] = None, is_key: bool = False, skip_tex: bool = False, skip_pdf: bool = False) -> str:
174def make_pdf(
175        variant: quizcomp.model.quiz.Variant,
176        out_dir: typing.Union[str, None] = None,
177        is_key: bool = False,
178        skip_tex: bool = False,
179        skip_pdf: bool = False,
180        ) -> str:
181    """ Make a PDF given a quiz variant. """
182
183    if (out_dir is None):
184        out_dir = edq.util.dirent.get_temp_path(prefix = 'quizcomp_pdf_', rm = False)
185
186    if (variant.variant_id is not None):
187        out_dir = os.path.join(out_dir, f"Variant - {variant.variant_id}")
188
189    edq.util.dirent.mkdir(out_dir)
190
191    out_json_path = os.path.join(out_dir, f"{variant.get_name()}.json")
192    variant.to_path(out_json_path)
193
194    image_dir = os.path.join(out_dir, 'images')
195
196    out_path = os.path.join(out_dir, f"{variant.get_name()}.tex")
197
198    if (not skip_tex):
199        converter = quizcomp.converter.tex.TexTemplateConverter(
200            answer_key = is_key,
201            image_base_dir = image_dir,
202            cleanup_images = True,
203        )
204        content = converter.convert_variant(variant)
205
206        edq.util.dirent.write_file(out_path, content)
207
208    if (not skip_pdf):
209        quizcomp.external.latex.compile(out_path)
210
211    return out_dir

Make a PDF given a quiz variant.

def modify_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
213def modify_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
214    """ Add PDF-based arguments to the CLI argument parser. """
215
216    parser.add_argument('path', metavar = 'PATH',
217        type = str,
218        help = 'The path to a JSON file.')
219
220    parser.add_argument('--out-dir', dest = 'out_dir',
221        action = 'store', type = str, default = '.',
222        help = 'The directory to put the output (default: %(default)s).')
223
224    parser.add_argument('--skip-tex', dest = 'skip_tex',
225        action = 'store_true', default = False,
226        help = 'Skip creating TeX files (default: %(default)s).')
227
228    parser.add_argument('--skip-pdf', dest = 'skip_pdf',
229        action = 'store_true', default = False,
230        help = 'Skip compiling PDFs from TeX (default: %(default)s).')
231
232    parser.add_argument('--variants', dest = 'variants',
233        action = 'store', type = int, default = 1,
234        help = 'The number of quiz variants to create (default: %(default)s).')
235
236    parser.add_argument('--skip-key', dest = 'skip_key',
237        action = 'store_true', default = False,
238        help = 'Skip creating the answer key (default: %(default)s).')
239
240    parser.add_argument('--seed', dest = 'seed',
241        action = 'store', type = int, default = None,
242        help = 'The random seed to use (defaults to a random seed).')
243
244    return parser

Add PDF-based arguments to the CLI argument parser.