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.