Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[latex] Add command problem_slides #413

Merged
merged 6 commits into from
Dec 27, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
49 changes: 38 additions & 11 deletions bin/latex.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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:
Expand All @@ -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,
Expand All @@ -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))
Expand All @@ -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
)


Expand Down
67 changes: 60 additions & 7 deletions bin/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -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.'
Expand All @@ -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',
Expand Down Expand Up @@ -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']:
Expand All @@ -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(
Copy link
Owner

@RagnarGrootKoerkamp RagnarGrootKoerkamp Dec 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this (the no_solutions flag) be more granular? It's annoying if things fail because no problem slides are present.

Or maybe better is to never make it an error if problem slides are missing? (But that's kinda tricky when you want slides for all problems?)

Also I checked an doing bt problem_slides at the contest level with some problem-slide.en.tex missing crashes in latex (because file not found). We should probably detect when there's not a single problem-slide present and then just Log that (without error). When only some files are present, it's fine to error IMO.

At the problem level it gives a nice message problem-slide.en.tex not found, so there all is good.

Otherwise LGTM! Thanks for making the PR :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good point! Will reduce some errors to warnings today or tomorrow 😄

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some extra checks 🙂 The behaviour at the problem level hasn't changed, this is the new behaviour at the contest level:

  • When no problem-slide.*.tex are present:
    • bt problem_slides will warn for all problems, followed by a failing LaTeX compilation (because the user is explicitly trying to compile them)
    • bt zip will log "No problem has problem-slide.*.tex, skipping problem slides"
  • When at least one problem-slide.*.tex is present:
    • Both bt problem_slides and bt zip will warn for the problems that do not have it, but will compile the rest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good. Thanks! Merging now.

contest,
problems,
tmpdir,
statement_language,
build_type=latex.PdfType.PROBLEM_SLIDE,
)

outfile = contest + '.zip'
Expand Down
17 changes: 17 additions & 0 deletions latex/contest-problem-slide.tex
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions latex/contest-solution.tex
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,7 @@
\renewcommand{\timelimit}{{%timelimit%}}
\input{{%problemdir%}/problem_statement/solution.\lang.tex}
\renewcommand{\problemlabel}{}
\renewcommand{\problemyamlname}{}
\renewcommand{\problemauthor}{}
\renewcommand{\timelimit}{}
\endgroup
103 changes: 103 additions & 0 deletions latex/problem-slides.tex
Original file line number Diff line number Diff line change
@@ -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}
Loading