From 9a9ab92bdbf7ceee767a027c85cc035ed8aa7ecb Mon Sep 17 00:00:00 2001 From: Maarten Sijm <9739541+mpsijm@users.noreply.github.com> Date: Sun, 20 Nov 2022 18:11:42 +0100 Subject: [PATCH] [latex] Add command problem_slides TODO: make problem_slides work for a single problem --- bin/export.py | 1 + bin/latex.py | 49 +++++++++++---- bin/tools.py | 67 ++++++++++++++++++--- latex/contest-problem-slide.tex | 17 ++++++ latex/contest-solution.tex | 3 + latex/problem-slides.tex | 103 ++++++++++++++++++++++++++++++++ 6 files changed, 222 insertions(+), 18 deletions(-) create mode 100644 latex/contest-problem-slide.tex create mode 100644 latex/problem-slides.tex diff --git a/bin/export.py b/bin/export.py index 76814b13..82174d22 100644 --- a/bin/export.py +++ b/bin/export.py @@ -258,6 +258,7 @@ def build_contest_zip(problems, zipfiles, outfile, statement_language): ] + list(Path('.').glob(f'contest*.{statement_language}.pdf')) + list(Path('.').glob(f'solutions*.{statement_language}.pdf')) + + list(Path('.').glob(f'problem-slides*.{statement_language}.pdf')) ): if Path(fname).is_file(): zf.write( diff --git a/bin/latex.py b/bin/latex.py index a448dc64..5c236972 100644 --- a/bin/latex.py +++ b/bin/latex.py @@ -4,13 +4,14 @@ import re import shutil import sys +from enum import Enum from pathlib import Path from typing import Optional from colorama import Fore, Style import config -from contest import contest_yaml +from contest import contest_yaml, problems_yaml import problem from util import ( copy_and_substitute, @@ -387,19 +388,27 @@ def find_logo() -> Path: return config.tools_root / 'latex' / 'images' / 'logo-not-found.pdf' +class PdfType(str, Enum): + PROBLEM = 'problem' + PROBLEM_SLIDE = 'problem-slide' + SOLUTION = 'solution' + + def build_contest_pdf( contest: str, problems: list["problem.Problem"], tmpdir: Path, language: str, - solutions=False, + build_type=PdfType.PROBLEM, web=False, ) -> bool: builddir = tmpdir / contest / 'latex' / language builddir.mkdir(parents=True, exist_ok=True) - build_type = 'solution' if solutions else 'problem' - main_file = 'solutions' if solutions else 'contest' + problem_slides = build_type == PdfType.PROBLEM_SLIDE + solutions = build_type == PdfType.SOLUTION + + main_file = 'problem-slides' if problem_slides else 'solutions' if solutions else 'contest' main_file += '-web.tex' if web else '.tex' bar = PrintBar(f'{main_file[:-3]}{language}.pdf') @@ -441,15 +450,17 @@ def build_contest_pdf( elif headertex.exists(): problems_data += f'\\input{{{headertex}}}\n' - local_per_problem_data = Path(f'contest-{build_type}.tex') + local_per_problem_data = Path(f'contest-{build_type.value}.tex') per_problem_data = ( local_per_problem_data if local_per_problem_data.is_file() - else config.tools_root / 'latex' / f'contest-{build_type}.tex' + else config.tools_root / 'latex' / f'contest-{build_type.value}.tex' ).read_text() + probyaml = problems_yaml() + for problem in problems: - if build_type == 'problem': + if build_type == PdfType.PROBLEM: prepare_problem(problem, language) if solutions: @@ -465,12 +476,28 @@ def build_contest_pdf( bar.warn(f'solution.{language}.tex not found', problem.name) continue + background = next( + (p['rgb'] for p in probyaml if p['id'] == str(problem.path) and 'rgb' in p), '#ffffff' + )[1:] + # Source: https://github.com/DOMjudge/domjudge/blob/095854650facda41dbb40966e70199840b887e33/webapp/src/Twig/TwigExtension.php#L1056 + foreground = ( + '000000' + if sum(int(background[i : i + 2], 16) for i in range(0, 6, 2)) > 450 + else 'ffffff' + ) + border = "".join( + ("00" + hex(max(0, int(background[i : i + 2], 16) - 64))[2:])[-2:] + for i in range(0, 6, 2) + ) problems_data += substitute( per_problem_data, { 'problemlabel': problem.label, 'problemyamlname': problem.settings.name[language].replace('_', ' '), 'problemauthor': problem.settings.author, + 'problembackground': background, + 'problemforeground': foreground, + 'problemborder': border, 'timelimit': get_tl(problem), 'problemdir': problem.path.absolute().as_posix(), 'problemdirname': problem.name, @@ -487,14 +514,14 @@ def build_contest_pdf( elif footertex.exists(): problems_data += f'\\input{{{footertex}}}\n' - (builddir / f'contest-{build_type}s.tex').write_text(problems_data) + (builddir / f'contest-{build_type.value}s.tex').write_text(problems_data) return build_latex_pdf(builddir, Path(main_file), language, bar) -def build_contest_pdfs(contest, problems, tmpdir, lang=None, solutions=False, web=False): +def build_contest_pdfs(contest, problems, tmpdir, lang=None, build_type=PdfType.PROBLEM, web=False): if lang: - return build_contest_pdf(contest, problems, tmpdir, lang, solutions, web) + return build_contest_pdf(contest, problems, tmpdir, lang, build_type, web) """Build contest PDFs for all available languages""" statement_languages = set.intersection(*(set(p.statement_languages) for p in problems)) @@ -519,7 +546,7 @@ def build_contest_pdfs(contest, problems, tmpdir, lang=None, solutions=False, we color_type=MessageType.FATAL, ) return all( - [build_contest_pdf(contest, problems, tmpdir, lang, solutions, web) for lang in languages] + build_contest_pdf(contest, problems, tmpdir, lang, build_type, web) for lang in languages ) diff --git a/bin/tools.py b/bin/tools.py index ff9cabf1..548683b3 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -407,7 +407,7 @@ def build_parser(): '--watch', '-w', action='store_true', - help='Continuously compile the pdf whenever a `problem_statement.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.', + help='Continuously compile the pdf whenever a `problem.*.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.', ) pdfparser.add_argument( '--open', @@ -420,6 +420,34 @@ def build_parser(): pdfparser.add_argument('--web', action='store_true', help='Create a web version of the pdf.') pdfparser.add_argument('-1', action='store_true', help='Only run the LaTeX compiler once.') + # Problem slides + pdfparser = subparsers.add_parser( + 'problem_slides', parents=[global_parser], help='Build the problem slides pdf.' + ) + # pdfparser.add_argument( + # '--all', + # '-a', + # action='store_true', + # help='Create problem statements for individual problems as well.', + # ) + pdfparser.add_argument('--no-timelimit', action='store_true', help='Do not print timelimits.') + pdfparser.add_argument( + '--watch', + '-w', + action='store_true', + help='Continuously compile the pdf whenever a `problem-slide.*.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files.', + ) + pdfparser.add_argument( + '--open', + '-o', + nargs='?', + const=True, + type=Path, + help='Open the continuously compiled pdf (with a specified program).', + ) + # pdfparser.add_argument('--web', action='store_true', help='Create a web version of the pdf.') + pdfparser.add_argument('-1', action='store_true', help='Only run the LaTeX compiler once.') + # Solution slides solparser = subparsers.add_parser( 'solutions', parents=[global_parser], help='Build the solution slides pdf.' @@ -442,7 +470,7 @@ def build_parser(): '--watch', '-w', action='store_true', - help='Continuously compile the pdf whenever a `solution.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.', + help='Continuously compile the pdf whenever a `solution.*.tex` changes. Note that this does not pick up changes to `*.yaml` configuration files. Further Note that this implies `--cp`.', ) solparser.add_argument( '--open', @@ -1060,7 +1088,16 @@ def run_parsed_arguments(args): if action in ['solutions']: success &= latex.build_contest_pdfs( - contest, problems, tmpdir, solutions=True, web=config.args.web + contest, problems, tmpdir, build_type=latex.PdfType.SOLUTION, web=config.args.web + ) + + if action in ['problem_slides']: + success &= latex.build_contest_pdfs( + contest, + problems, + tmpdir, + build_type=latex.PdfType.PROBLEM_SLIDE, + web=config.args.web, ) if action in ['zip']: @@ -1074,11 +1111,27 @@ def run_parsed_arguments(args): contest, problems, tmpdir, statement_language, web=True ) if not config.args.no_solutions: - success &= latex.build_contest_pdfs( - contest, problems, tmpdir, statement_language, solutions=True + success &= latex.build_contest_pdf( + contest, + problems, + tmpdir, + statement_language, + build_type=latex.PdfType.SOLUTION, + ) + success &= latex.build_contest_pdf( + contest, + problems, + tmpdir, + statement_language, + build_type=latex.PdfType.SOLUTION, + web=True, ) - success &= latex.build_contest_pdfs( - contest, problems, tmpdir, statement_language, solutions=True, web=True + success &= latex.build_contest_pdf( + contest, + problems, + tmpdir, + statement_language, + build_type=latex.PdfType.PROBLEM_SLIDE, ) outfile = contest + '.zip' diff --git a/latex/contest-problem-slide.tex b/latex/contest-problem-slide.tex new file mode 100644 index 00000000..235bb078 --- /dev/null +++ b/latex/contest-problem-slide.tex @@ -0,0 +1,17 @@ +\begingroup\graphicspath{{{%problemdir%}/problem_statement/}} + \renewcommand{\problemlabel}{{%problemlabel%}} + \renewcommand{\problemyamlname}{{%problemyamlname%}} + \renewcommand{\problemauthor}{{%problemauthor%}} + \renewcommand{\problembackground}{{%problembackground%}} + \renewcommand{\problemforeground}{{%problemforeground%}} + \renewcommand{\problemborder}{{%problemborder%}} + \renewcommand{\timelimit}{{%timelimit%}} + \input{{%problemdir%}/problem_statement/problem-slide.\lang.tex} + \renewcommand{\problemlabel}{} + \renewcommand{\problemyamlname}{} + \renewcommand{\problemauthor}{} + \renewcommand{\problembackground}{} + \renewcommand{\problemforeground}{} + \renewcommand{\problemborder}{} + \renewcommand{\timelimit}{} +\endgroup diff --git a/latex/contest-solution.tex b/latex/contest-solution.tex index af12fc9c..fd11f9a8 100644 --- a/latex/contest-solution.tex +++ b/latex/contest-solution.tex @@ -5,4 +5,7 @@ \renewcommand{\timelimit}{{%timelimit%}} \input{{%problemdir%}/problem_statement/solution.\lang.tex} \renewcommand{\problemlabel}{} + \renewcommand{\problemyamlname}{} + \renewcommand{\problemauthor}{} + \renewcommand{\timelimit}{} \endgroup diff --git a/latex/problem-slides.tex b/latex/problem-slides.tex new file mode 100644 index 00000000..a37d61af --- /dev/null +++ b/latex/problem-slides.tex @@ -0,0 +1,103 @@ +\documentclass[rgb,dvipsnames,aspectratio=169,9pt,t]{beamer} + +\usepackage[T1, OT1]{fontenc} +\DeclareTextSymbolDefault{\dh}{T1} +\usepackage[english]{babel} +\usepackage{lmodern} + +%------------------------------------------------------------------------------- +% The following are required for most problems: +%------------------------------------------------------------------------------- +\usepackage{amsmath,amssymb} +\usepackage{pgf,tikz} +\usepackage{mathrsfs} +\usetikzlibrary{arrows} +\usetikzlibrary{shapes} +\usetikzlibrary{backgrounds} +\usetikzlibrary{patterns} +\usetikzlibrary{positioning} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\pgfplotsset{compat=1.15} +\usepackage{graphicx} +\usepackage{listings} +%\usepackage{subcaption} +\usepackage{algorithm} +\usepackage[makeroom]{cancel} +\usepackage[noend]{algpseudocode} +\usepackage{standalone} +\usepackage{ifthen} +\usepackage{tcolorbox} +\usepackage{upquote} % For ' in samples +\usepackage[autoplay,controls,loop,poster=last]{animate} + +\lstset{ + backgroundcolor=\color{white}, + tabsize=4, + language=python, + basicstyle=\footnotesize\ttfamily, + breaklines=true, + keywordstyle=\color[rgb]{0, 0, 1}, + commentstyle=\color[rgb]{0, 0.5, 0}, + stringstyle=\color{red} +} + +\newcommand{\timelimit}{0} +\newcommand{\problemlabel}{} % Empty to hide activity chart +\newcommand{\problemauthor}{Problem author} +\newcommand{\problembackground}{} +\newcommand{\problemforeground}{} +\newcommand{\problemborder}{} +% TODO: Clean these up +\newcommand{\problemyamlname}{Problem name} +\newcommand{\fullproblemtitle}{\problemlabel: \problemyamlname} +\newcommand{\problemtitle}{\problemyamlname} + +\usetheme[numbering=none,block=fill]{metropolis} + +\newcommand{\illustration}[3]{ + \begin{column}[T]{#1\textwidth} + \includegraphics[width=\textwidth]{#2} + \ifstrempty{#3}{ + \vspace{-5pt} + }{ + \begin{flushright} + \vspace{-5pt} + \tiny #3 + \end{flushright} + } + \end{column} +} + +\setbeamertemplate{frametitle}{% + \nointerlineskip% + \vspace{1em}% + \begin{minipage}{0.06\paperwidth}% + \begin{tikzpicture} + \definecolor{problembackground}{HTML}{\problembackground} + \definecolor{problemforeground}{HTML}{\problemforeground} + \definecolor{problemborder}{HTML}{\problemborder} + % Hack: the square for problem label "M" would be too wide so use `\huge` instead of `\Huge` + \node[rectangle,rounded corners,thick,minimum size=2em,draw=problemborder,fill=problembackground,text=problemforeground] (0, 0) {\if M\problemlabel\huge\else\Huge\fi\problemlabel}; + \end{tikzpicture} + \end{minipage}% + \begin{minipage}{0.8\paperwidth}% + \color{black}% + \ifdefempty{\problemlabel}{% + \insertframetitle\strut% + }{% + \problemtitle% + \\[0.3em]% + \tiny% + Time limit: \timelimit{}s% + \quad\quad% + Problem Author: \problemauthor% + \strut% + }% + \end{minipage}% + \hfill% +} + +\begin{document} +\input{./contest-problem-slides.tex} +\end{document}