diff --git a/MANIFEST.in b/MANIFEST.in
index 277fb2252317b05335ed361217ecf7f2b5c2291b..c9341bba4ca787a2c0c9dfa90bda11a5adc15785 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,2 +1,2 @@
 recursive-include mo/web/templates *
-recursive-include mo/web/static *
+recursive-include mo/tex *
diff --git a/bin/run-jobs b/bin/run-jobs
index 60635f4598d8fe90ce385f523f2aa3f33588c13b..c72a583e88a317567d1188392e8ff650790e8a58 100755
--- a/bin/run-jobs
+++ b/bin/run-jobs
@@ -1,12 +1,23 @@
 #!/usr/bin/env python3
 
 import mo.jobs
-import mo.util
+from mo.util import die, init_standalone
 import argparse
 
 parser = argparse.ArgumentParser(description='Spustí joby ve frontě')
+parser.add_argument('-j', '--job', type=int, metavar='ID', help='Spustí konkrétní job')
+parser.add_argument('-r', '--retry', default=False, action='store_true', help='Znovu spustí dokončený job')
 
 args = parser.parse_args()
 
-mo.util.init_standalone()
-mo.jobs.process_jobs()
+init_standalone()
+
+if args.job is None:
+    if args.retry:
+        die("Přepínač --retry lze použít jen s --job")
+    mo.jobs.process_jobs()
+else:
+    tj = mo.jobs.TheJob(args.job)
+    if not tj.load():
+        die("Tento job neexistuje")
+    tj.run(retry=args.retry)
diff --git a/constraints.txt b/constraints.txt
index 9c4dbd343b0917fdd6123a6f3370488ea8a965fc..6499845736ec89bccc04f150c16a7a4af437fb62 100644
--- a/constraints.txt
+++ b/constraints.txt
@@ -9,21 +9,29 @@ Flask==1.1.2
 Flask-Bootstrap==3.3.7.1
 Flask-SQLAlchemy==2.4.4
 Flask-WTF==0.14.3
+importlib-metadata==4.6.0
 itsdangerous==1.1.0
 Jinja2==2.11.2
 lxml==4.6.2
-markdown==3.3.4
+Markdown==3.3.4
 MarkupSafe==1.1.1
+packaging==21.0
 pikepdf==2.3.0
 Pillow==8.1.0
 pkg-resources==0.0.0
 psycopg2==2.8.6
 pycparser==2.20
+pyparsing==2.4.7
 python-dateutil==2.8.1
+python-poppler==0.2.2
 pytz==2020.5
+pyzbar==0.1.8
 six==1.15.0
 SQLAlchemy==1.3.22
+typing-extensions==3.10.0.0
 uwsgidecorators==1.1.0
 visitor==0.1.3
+webencodings==0.5.1
 Werkzeug==1.0.1
 WTForms==2.3.3
+zipp==3.5.0
diff --git a/db/db.ddl b/db/db.ddl
index 161edc2ef218e4a96f33092d88929a043f571ed9..6a786dd633d89bf6631edd6c7787d9525c40243c 100644
--- a/db/db.ddl
+++ b/db/db.ddl
@@ -308,10 +308,13 @@ CREATE INDEX log_type_id_index ON log (type, id);
 
 CREATE TYPE job_type AS ENUM (
 	'download_submits',
-	'upload_feedback'
+	'upload_feedback',
+	'create_protocols',
+	'process_scans'
 );
 
 CREATE TYPE job_state AS ENUM (
+	'preparing',
 	'ready',
 	'running',
 	'done',				-- Hotovo, out_json a out_file jsou platné
@@ -407,3 +410,20 @@ CREATE VIEW region_task_stats AS
 	JOIN solutions s USING(user_id, task_id)
 	JOIN region_descendants rd ON rd.descendant = c.place_id
 	GROUP BY r.round_id, rd.region, t.task_id;
+
+-- Stav zpracování scanů (vázaný na joby)
+
+CREATE TABLE scan_pages (
+	job_id		int		NOT NULL REFERENCES jobs(job_id) ON DELETE CASCADE,
+	file_nr		int		NOT NULL,					-- co to je za stránku (od 0)
+	page_nr		int		NOT NULL,
+	user_id		int		DEFAULT NULL REFERENCES users(user_id),		-- přiřazení účastníkovi a úloze
+	task_id		int		DEFAULT NULL REFERENCES tasks(task_id),
+	seq_id		int		NOT NULL,					-- pořadové číslo v rámci úlohy (od 0)
+	-- Pokud user_id i task_id jsou NULL, seq_id znamená:
+	--	-1	pro stránku vyžadující pozornost
+	--	-2	pro prázdnou stránku
+	--	-3	pro pokračovací stránku
+	--	-4	pro stránku, která nepatří do této soutěže
+	UNIQUE (job_id, file_nr, page_nr)
+);
diff --git a/db/upgrade-20210701.sql b/db/upgrade-20210701.sql
new file mode 100644
index 0000000000000000000000000000000000000000..b226fba273bc7edc886805b4ae6497b37685d9f5
--- /dev/null
+++ b/db/upgrade-20210701.sql
@@ -0,0 +1,16 @@
+SET ROLE 'mo_osmo';
+
+ALTER TYPE job_type ADD VALUE 'create_protocols';
+ALTER TYPE job_type ADD VALUE 'process_scans';
+
+ALTER TYPE job_state ADD VALUE 'preparing';
+
+CREATE TABLE scan_pages (
+	job_id		int		NOT NULL REFERENCES jobs(job_id) ON DELETE CASCADE,
+	file_nr		int		NOT NULL,					-- co to je za stránku
+	page_nr		int		NOT NULL,
+	user_id		int		DEFAULT NULL REFERENCES users(user_id),		-- přiřazení účastníkovi a úloze
+	task_id		int		DEFAULT NULL REFERENCES tasks(task_id),
+	seq_id		int		NOT NULL,					-- pořadové číslo v rámci úlohy
+	UNIQUE (job_id, file_nr, page_nr)
+);
diff --git a/etc/config.py.example b/etc/config.py.example
index 5e3f877256421176faf0c10d352dcf654d3bfc8f..37637e91c98851e7623d08bb192d3e8d85267420 100644
--- a/etc/config.py.example
+++ b/etc/config.py.example
@@ -48,6 +48,9 @@ GC_PERIOD = 60
 # Za jak dlouho expiruje dokončená dávka [min]
 JOB_EXPIRATION = 5
 
+# Některé dávky (analýza scanů) mají delší expiraci [min]
+JOB_EXPIRATION_LONG = 1440
+
 # Kolik nejvýše dovolujeme registrací za minutu
 REG_MAX_PER_MINUTE = 10
 
diff --git a/mo/db.py b/mo/db.py
index ba1e334f34418f9e0eccc44efb51ab5a75091052..a756eb8bf1e94d11dec586e4a8b6c65f04383056 100644
--- a/mo/db.py
+++ b/mo/db.py
@@ -5,6 +5,7 @@ import datetime
 import decimal
 from enum import Enum as PythonEnum, auto
 import locale
+import os
 import re
 from sqlalchemy import \
     Boolean, Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, \
@@ -22,6 +23,7 @@ from sqlalchemy.sql.sqltypes import Numeric
 from typing import Optional, List, Tuple
 
 import mo
+import mo.config as config
 from mo.place_level import place_levels, PlaceLevel
 from mo.util_format import timeformat_short, timedelta, time_and_timedelta
 
@@ -685,9 +687,12 @@ class Solution(Base):
 class JobType(MOEnum):
     download_submits = auto()
     upload_feedback = auto()
+    create_protocols = auto()
+    process_scans = auto()
 
 
 class JobState(MOEnum):
+    preparing = auto()
     ready = auto()
     running = auto()
     done = auto()
@@ -698,6 +703,7 @@ class JobState(MOEnum):
 
 
 job_state_names = {
+    JobState.preparing: 'připravuje se',
     JobState.ready: 'čeká na spuštění',
     JobState.running: 'zpracovává se',
     JobState.done: 'dokončena',
@@ -724,6 +730,14 @@ class Job(Base):
 
     user = relationship('User')
 
+    def dir_path(self) -> str:
+        """Adresář se soubory příslušejícími k jobu."""
+        # Nepoužíváme mo.util.data_dir, abychom se vyhnuli cyklické závislosti modulů.
+        return os.path.join(config.DATA_DIR, 'jobs', str(self.job_id))
+
+    def file_path(self, name: str) -> str:
+        return os.path.join(self.dir_path(), name)
+
 
 class Message(Base):
     __tablename__ = 'messages'
@@ -806,6 +820,29 @@ class RegionTaskStat(Base):
     task = relationship('Task')
 
 
+class ScanPage(Base):
+    __tablename__ = 'scan_pages'
+
+    job_id = Column(Integer, ForeignKey('jobs.job_id', ondelete='CASCADE'), primary_key=True, nullable=False)
+    file_nr = Column(Integer, primary_key=True, nullable=False)
+    page_nr = Column(Integer, primary_key=True, nullable=False)
+    user_id = Column(Integer, ForeignKey('users.user_id'))
+    task_id = Column(Integer, ForeignKey('tasks.task_id'))
+    seq_id = Column(Integer, nullable=False)
+    UniqueConstraint('job_id', 'file_nr', 'page_nr')
+
+    job = relationship('Job')
+    user = relationship('User')
+    task = relationship('Task')
+
+
+# Speciální seq_id ve ScanPage
+SCAN_PAGE_FIX = -1
+SCAN_PAGE_EMPTY = -2
+SCAN_PAGE_CONTINUE = -3
+SCAN_PAGE_UFO = -4
+
+
 _engine: Optional[Engine] = None
 _session: Optional[Session] = None
 flask_db: Any = None
diff --git a/mo/jobs/__init__.py b/mo/jobs/__init__.py
index 31560882195ddd2f5bc8d4ddabb0970a47c1d983..2d7da1a06762287ec011e153a8292657d6acbaab 100644
--- a/mo/jobs/__init__.py
+++ b/mo/jobs/__init__.py
@@ -2,6 +2,7 @@
 
 from datetime import timedelta
 import os
+import shutil
 from sqlalchemy import or_
 from typing import Optional, Dict, Callable, List
 
@@ -19,20 +20,6 @@ def send_notify():
     logger.debug('Job: Není komu poslat notifikaci')
 
 
-def job_file_path(name: str) -> str:
-    return os.path.join(mo.util.data_dir('jobs'), name)
-
-
-def job_file_size(name: Optional[str]) -> Optional[int]:
-    if name is None:
-        return None
-
-    try:
-        return os.path.getsize(job_file_path(name))
-    except OSError:
-        return -1
-
-
 class TheJob:
     """Job z pohledu Pythonu."""
 
@@ -40,6 +27,7 @@ class TheJob:
     job_id: Optional[int]
     gatekeeper: Optional[mo.rights.Gatekeeper]
     errors: List[str]
+    expires_in_minutes: int
 
     def __init__(self, job_id: Optional[int] = None):
         """Pokud chceme pracovat s existujícím jobem, zadáme jeho ID."""
@@ -47,40 +35,51 @@ class TheJob:
         self.errors = []
 
     def load(self) -> db.Job:
-        sess = db.get_session()
-        self.job = sess.query(db.Job).with_for_update().get(self.job_id)
+        if getattr(self, 'job', None) is None:
+            sess = db.get_session()
+            self.job = sess.query(db.Job).with_for_update().get(self.job_id)
         return self.job
 
     def create(self, type: db.JobType, for_user: db.User) -> db.Job:
-        self.job = db.Job(type=type, state=db.JobState.ready, user=for_user)
-        return self.job
-
-    def attach_file(self, tmp_name: str, suffix: str):
-        """Vytvoří hardlink na daný pracovní soubor v adresáři jobů."""
-
-        full_name = mo.util.link_to_dir(tmp_name, mo.util.data_dir('jobs'), suffix=suffix)
-        name = os.path.basename(full_name)
-        logger.debug(f'Job: Příloha {tmp_name} -> {name}')
-        return name
+        self.job = db.Job(type=type, state=db.JobState.preparing, user=for_user)
 
-    def submit(self):
+        # Do DB přidáváme nehotový job, protože potřebujeme znát job_id pro založení adresáře
         sess = db.get_session()
         sess.add(self.job)
         sess.flush()
         self.job_id = self.job.job_id
+
         logger.info(f'Job: Vytvořen job #{self.job_id} pro uživatele #{self.job.user_id}')
-        sess.commit()
+
+        job_dir = self.job.dir_path()
+        if os.path.exists(job_dir):
+            # Hypoteticky by se mohlo stát, že se recykluje job_id od jobu, jehož
+            # vytvoření selhalo před commitem. Zkusíme tedy smazat prázdný adresář.
+            os.rmdir(job_dir)
+        os.mkdir(job_dir)
+
+        return self.job
+
+    def attach_file(self, tmp_name: str, attachment_name: str) -> str:
+        """Vytvoří hardlink na daný pracovní soubor v adresáři jobu."""
+
+        full_name = self.job.file_path(attachment_name)
+        os.link(tmp_name, full_name)
+        logger.debug(f'Job: Příloha {tmp_name} -> {full_name}')
+        return attachment_name
+
+    def submit(self):
+        self.job.state = db.JobState.ready
+        db.get_session().commit()
         send_notify()
 
     def _finish_remove(self):
         sess = db.get_session()
         job = self.job
 
-        if job.in_file is not None:
-            mo.util.unlink_if_exists(job_file_path(job.in_file))
-
-        if job.out_file is not None:
-            mo.util.unlink_if_exists(job_file_path(job.out_file))
+        job_dir = self.job.dir_path()
+        if os.path.exists(job_dir):
+            shutil.rmtree(job_dir)
 
         sess.delete(job)
         sess.commit()
@@ -106,21 +105,42 @@ class TheJob:
         logger.info(f'Job: >> {msg}')
         self.errors.append(msg)
 
-    def run(self):
+    def _check_runnable(self, retry: bool) -> Optional[str]:
+        s = self.job.state
+        if s == db.JobState.ready:
+            return None
+        elif s == db.JobState.running:
+            # Může se stát, že ho mezitím začal vyřizovat jiný proces
+            return 'právě běží'
+        elif s in (db.JobState.done, db.JobState.failed):
+            return None if retry else 'je už hotový'
+        else:
+            return 'je v neznámém stavu'
+
+    def run(self, retry: bool = False):
         sess = db.get_session()
-        if not self.load() or self.job.state != db.JobState.ready:
-            # Někdo ho mezitím smazal nebo vyřídil
-            logger.info(f'Job: Job #{self.job_id} vyřizuje někdo jiný')
+        if not self.load():
+            # Někdo ho mezitím smazal
+            logger.info(f'Job: Job #{self.job_id} neexistuje')
+            sess.rollback()
+            return
+
+        reject_reason = self._check_runnable(retry)
+        if reject_reason is not None:
+            logger.info(f'Job: Job #{self.job_id} {reject_reason}')
             sess.rollback()
             return
 
         job = self.job
         logger.info(f'Job: Spouštím job #{job.job_id} ({job.type}) uživatele #{job.user_id}')
         job.state = db.JobState.running
+        job.finished_at = None
+        job.expires_at = None
         sess.commit()
 
         try:
             self.gatekeeper = mo.rights.Gatekeeper(job.user)
+            self.expires_in_minutes = config.JOB_EXPIRATION
             _handler_table[job.type](self)
             if self.errors:
                 logger.info(f'Job: Neúspěšně dokončen job #{job.job_id} ({job.result})')
@@ -137,7 +157,7 @@ class TheJob:
             job.result = 'Interní chyba, informujte prosím správce systému.'
 
         job.finished_at = mo.util.get_now()
-        job.expires_at = job.finished_at + timedelta(minutes=config.JOB_EXPIRATION)
+        job.expires_at = job.finished_at + timedelta(minutes=self.expires_in_minutes)
         sess.commit()
 
 
@@ -189,4 +209,5 @@ def job_handler(type: db.JobType):
 
 
 # Moduly implementující jednotlivé typy jobů
+import mo.jobs.protocols
 import mo.jobs.submit
diff --git a/mo/jobs/protocols.py b/mo/jobs/protocols.py
new file mode 100644
index 0000000000000000000000000000000000000000..0bf16b94d719476d9ff6f34f92b0897c9ccdb7c3
--- /dev/null
+++ b/mo/jobs/protocols.py
@@ -0,0 +1,354 @@
+# Implementace jobů na práci s protokoly
+
+from PIL import Image
+from dataclasses import dataclass
+import multiprocessing
+import os
+import poppler
+import pyzbar.pyzbar as pyzbar
+import re
+from sqlalchemy import delete
+from sqlalchemy.orm import joinedload
+from sqlalchemy.orm.query import Query
+import subprocess
+from typing import List, Optional
+
+import mo
+import mo.config as config
+import mo.db as db
+from mo.jobs import TheJob, job_handler
+from mo.util import logger, part_path
+import mo.util_format
+
+
+#
+# Job create_protocols: Vygeneruje formuláře protokolů
+#
+# Vstupní JSON:
+#        { 'contest_id': ID contestu,
+#          'site_id': ID soutěžního místa nebo none,
+#          'task_ids': [task_id, ...],
+#          'num_universal': počet papírů s univerzalní hlavičkou,
+#          'num_blank': pocet pokračovacích papírů,
+#        }
+#
+# Výstupní JSON:
+#        null
+#
+
+
+def schedule_create_protocols(contest: db.Contest, site: Optional[db.Place], for_user: db.User, tasks: List[db.Task], num_universal: int, num_blank: int):
+    place = site or contest.place
+
+    the_job = TheJob()
+    job = the_job.create(db.JobType.create_protocols, for_user)
+    job.description = f'Formuláře protokolů {contest.round.round_code_short()} {place.name}'
+    job.in_json = {
+        'contest_id': contest.contest_id,
+        'site_id': site.place_id if site else None,
+        'task_ids': [t.task_id for t in tasks],
+        'num_universal': num_universal,
+        'num_blank': num_blank,
+    }
+    the_job.submit()
+
+
+def tex_arg(s: str) -> str:
+    # Primitivní escapování do TeXu. Nesnaží se ani tak o věrnou intepretaci všech znaků,
+    # jako o zabránění pádu TeXu kvůli divným znakům.
+    s = re.sub(r'[\\{}#$%^~]', '?', s)
+    s = re.sub(r'([&_])', r'\\\1', s)
+    return '{' + s + '}'
+
+
+def _get_user_id_query(contest: db.Contest, site_id: Optional[int]) -> Query:
+    q = db.get_session().query(db.Participation.user_id).filter_by(contest=contest, state=db.PartState.active)
+    if site_id is not None:
+        q = q.filter_by(place_id=site_id)
+    return q
+
+
+def _get_pants(contest: db.Contest, site_id: Optional[int]) -> List[db.Participant]:
+    user_id_subq = _get_user_id_query(contest, site_id).subquery()
+
+    pants = (db.get_session().query(db.Participant)
+             .options(joinedload(db.Participant.user), joinedload(db.Participant.school_place))
+             .filter_by(year=config.CURRENT_YEAR)
+             .filter(db.Participant.user_id.in_(user_id_subq))
+             .all())
+    pants.sort(key=lambda p: p.user.sort_key())
+
+    return pants
+
+
+@job_handler(db.JobType.create_protocols)
+def handle_create_protocols(the_job: TheJob):
+    job = the_job.job
+    assert job.in_json is not None
+    contest_id: int = job.in_json['contest_id']  # type: ignore
+    site_id: int = job.in_json['site_id']        # type: ignore
+    task_ids: List[int] = job.in_json['task_ids']      # type: ignore
+    num_universal: int = job.in_json['num_universal']    # type: ignore
+    num_blank: int = job.in_json['num_blank']    # type: ignore
+
+    sess = db.get_session()
+    contest = sess.query(db.Contest).options(joinedload(db.Contest.round)).get(contest_id)
+    assert contest is not None
+    round = contest.round
+
+    pants = _get_pants(contest, site_id)
+    tasks = sess.query(db.Task).filter_by(round=round).filter(db.Task.task_id.in_(task_ids)).order_by(db.Task.code).all()
+
+    pages = []
+    for p in pants:
+        for t in tasks:
+            args = [
+                ':'.join(['MO', round.round_code_short(), t.code, str(p.user_id)]),
+                p.user.full_name(),
+                p.grade,
+                p.school_place.name or '???',
+                t.code,
+            ]
+            pages.append('\\proto' + "".join([tex_arg(x) for x in args]))
+
+    for _ in range(num_universal):
+        pages.append('\\universal')
+
+    for _ in range(num_blank):
+        pages.append('\\blank')
+
+    if not pages:
+        the_job.error("Nebyly vyžádány žádné protokoly")
+        return
+
+    temp_dir = job.dir_path()
+    logger.debug('Job: Vytvářím protokoly v %s (%s listů)', temp_dir, len(pages))
+
+    tex_src = os.path.join(temp_dir, 'protokoly.tex')
+    with open(tex_src, 'w') as f:
+        f.write('\\input protokol.tex\n\n')
+        kolo = f'{round.name} {round.year}. ročníku Matematické olympiády'
+        kat = f'Kategorie {round.category}'
+        if round.level > 0:
+            kat += ', ' + contest.place.name
+        f.write('\\def\\kolo' + tex_arg(kolo) + '\n\n')
+        f.write('\\def\\kat' + tex_arg(kat) + '\n\n')
+
+        for p in pages:
+            f.write(p + '\n')
+
+        f.write('\n\\bye\n')
+
+    env = dict(os.environ)
+    env['TEXINPUTS'] = part_path('tex') + '//:'
+
+    subprocess.run(
+        ['luatex', '--interaction=errorstopmode', 'protokoly.tex'],
+        check=True,
+        cwd=temp_dir,
+        env=env,
+        stdin=subprocess.DEVNULL,
+        stdout=subprocess.DEVNULL,
+        stderr=subprocess.DEVNULL,
+    )
+
+    job.out_file = 'protokoly.pdf'
+    job.result = 'Celkem ' + mo.util_format.inflect_number(len(pages), 'list', 'listy', 'listů')
+
+
+#
+# Job process_scans: Zpracuje nascanované protokoly
+#
+# Vstupní JSON:
+#        { 'contest_id': ID contestu,
+#          'site_id': ID soutěžního místa nebo none,
+#          'task_ids': [task_id, ...],
+#          'in_files': [názvy vstupních souborů]
+#        }
+#
+# Výstupní JSON:
+#        null
+#
+# Výstupn soubory:
+#       p-{file_nr:02d}-{page_nr:04d}-(full|small).png
+#
+
+
+def schedule_process_scans(contest: db.Contest, site: Optional[db.Place], for_user: db.User, tasks: List[db.Task], in_file_names: List[str]):
+    place = site or contest.place
+
+    the_job = TheJob()
+    job = the_job.create(db.JobType.process_scans, for_user)
+    job.description = f'Zpracování scanů {contest.round.round_code_short()} {place.name}'
+
+    in_files = []
+    num_files = 0
+    for ifn in in_file_names:
+        num_files += 1
+        in_name = f'input-{num_files:03d}.pdf'
+        the_job.attach_file(ifn, in_name)
+        in_files.append(in_name)
+    assert in_files
+
+    job.in_json = {
+        'contest_id': contest.contest_id,
+        'site_id': site.place_id if site else None,
+        'task_ids': [t.task_id for t in tasks],
+        'in_files': in_files,
+    }
+    the_job.submit()
+
+
+@dataclass
+class ScanJobArgs:
+    in_path: str
+    out_prefix: str
+
+
+@dataclass
+class ScanJobPage:
+    code: Optional[str]
+
+
+@job_handler(db.JobType.process_scans)
+def handle_process_scans(the_job: TheJob):
+    job = the_job.job
+    assert job.in_json is not None
+    contest_id = job.in_json['contest_id']  # type: ignore
+    site_id = job.in_json['site_id']        # type: ignore
+    task_ids = job.in_json['task_ids']      # type: ignore
+    in_files: List[str] = job.in_json['in_files']  # type: ignore
+
+    sess = db.get_session()
+    contest = sess.query(db.Contest).options(joinedload(db.Contest.round)).get(contest_id)
+    assert contest is not None
+    round = contest.round
+    round_code = round.round_code_short()
+
+    user_ids = set(u[0] for u in _get_user_id_query(contest, site_id).all())
+
+    tasks = sess.query(db.Task).filter(db.Task.task_id.in_(task_ids)).all()
+    tasks_by_code = {t.code: t for t in tasks}
+
+    # Jelikož se plánujeme zamyslet na dlouhou dobu, uzavřeme databázovou session.
+    sess.commit()
+
+    with multiprocessing.Pool(1) as pool:
+        args = [ScanJobArgs(in_path=job.file_path(fn),
+                            out_prefix=job.file_path(f'p-{fi:02d}'))
+                for fi, fn in enumerate(in_files)]
+        results = pool.map(_process_scan_file, args)
+
+    def _parse_code(pr: ScanJobPage, sp: db.ScanPage) -> Optional[str]:
+        if pr.code is None:
+            return None
+
+        fields = pr.code.split(':')
+        if fields[0] != 'MO':
+            return 'Neznámý prefix'
+
+        if len(fields) == 2:
+            if fields[1] == '*':
+                # Univerzální hlavička úlohy
+                sp.seq_id = db.SCAN_PAGE_FIX
+                return None
+            if fields[1] == '+':
+                # Pokračovací papír s kódem
+                sp.seq_id = db.SCAN_PAGE_CONTINUE
+                return None
+
+        elif len(fields) == 4:
+            if not fields[3].isnumeric():
+                return 'User ID není číslo'
+            user_id = int(fields[3])
+
+            if fields[1] != round_code:
+                return 'Nesouhlasí kód kola'
+            if fields[2] not in tasks_by_code:
+                return 'Neznámá úloha'
+            if user_id not in user_ids:
+                return 'Neznámý účastník'
+            sp.user_id = user_id
+            sp.task_id = tasks_by_code[fields[2]].task_id
+            sp.seq_id = 0
+            return None
+
+        return 'Neznamý formát kódu'
+
+    # Pokud jsme job spustili podruhé (ruční retry), chceme smazat všechny záznamy v scan_pages.
+    # Pozor, nesynchronizujeme ORM, ale nevadí to, protože v této chvíli mame čerstvou session.
+    conn = sess.connection()
+    conn.execute(delete(db.ScanPage.__table__).where(db.ScanPage.job_id == job.job_id))
+
+    num_pages = 0
+    for fi, fn in enumerate(in_files):
+        for pi, pr in enumerate(results[fi]):
+            sp = db.ScanPage(
+                job_id=job.job_id,
+                file_nr=fi,
+                page_nr=pi,
+                seq_id=db.SCAN_PAGE_FIX,
+            )
+
+            err = _parse_code(pr, sp)
+            if err is not None:
+                logger.debug(f'Scan: {fi}/{pi} ({pr.code}): {err}')
+                sp.seq_id = db.SCAN_PAGE_UFO
+
+            sess.add(sp)
+            num_pages += 1
+
+    job.result = 'Celkem ' + mo.util_format.inflect_number(num_pages, 'strana', 'strany', 'stran')
+    the_job.expires_in_minutes = config.JOB_EXPIRATION_LONG
+
+
+def _process_scan_file(args: ScanJobArgs) -> List[ScanJobPage]:
+    # Zpracuje jeden soubor se scany. Běží v odděleném procesu.
+
+    # FIXME: Ošetření chyb
+    logger.debug(f'Scan: Analyzuji soubor {args.in_path}')
+    pdf = poppler.load_from_file(args.in_path)
+
+    renderer = poppler.PageRenderer()
+    renderer.set_render_hint(poppler.RenderHint.antialiasing, True)
+    renderer.set_render_hint(poppler.RenderHint.text_antialiasing, True)
+    dpi = 300
+
+    output = []
+    for page_nr in range(pdf.pages):
+        page = pdf.create_page(page_nr)
+        page_img = renderer.render_page(page, xres=dpi, yres=dpi)
+
+        full_img = Image.frombytes(
+            "RGBA",
+            (page_img.width, page_img.height),
+            page_img.data,
+            "raw",
+            str(page_img.format),
+        )
+        del page_img
+
+        full_img = full_img.convert('L')               # Grayscale
+        full_size = full_img.size
+
+        codes = pyzbar.decode(full_img, symbols=[pyzbar.ZBarSymbol.QRCODE])
+        codes = [c for c in codes if c.type == 'QRCODE' and c.data.startswith(b'MO:')]
+        qr = None
+        if codes:
+            if len(codes) > 1:
+                logger.warning(f'Scan: Strana #{page_nr} obsahuje více QR kódů')
+            code = codes[0]
+            qr = code.data.decode('US-ASCII')
+            # FIXME: Tady by se dala podle kódu otočit stránka
+
+        output.append(ScanJobPage(code=qr))
+
+        full_img.save(f'{args.out_prefix}-{page_nr:04d}-full.png')
+
+        # FIXME: Potřebujeme vytvářet miniaturu?
+        small_img = full_img.resize((full_size[0] // 4, full_size[1] // 4))
+        small_img.save(f'{args.out_prefix}-{page_nr:04d}-small.png')
+
+        logger.debug(f'Scan: Strana #{page_nr}: {qr}')
+
+    return output
diff --git a/mo/jobs/submit.py b/mo/jobs/submit.py
index c6e52c2f849414360b90fc6e0fb123993ec510eb..136d17f07449464558216267a80c2ea988f23d24 100644
--- a/mo/jobs/submit.py
+++ b/mo/jobs/submit.py
@@ -13,37 +13,41 @@ import werkzeug.utils
 import zipfile
 
 import mo.db as db
-from mo.jobs import TheJob, job_handler, job_file_path
+from mo.jobs import TheJob, job_handler
 from mo.submit import Submitter, SubmitException
 from mo.util import logger, data_dir
 from mo.util_format import inflect_number, inflect_by_number, data_size
 
 
-def schedule_download_submits(paper_ids: List[int], description: str, for_user: db.User, want_subdirs: bool):
+#
+# Job download_submits: Zazipuje vybrané papíry
+#
+# Vstupní JSON:
+#        { 'papers': [ seznam paper_id ke stažení ],
+#          'want_feedback': true/false,
+#          'out_name': jméno výstupního souboru bez přípony,
+#        }
+#
+# Výstupní JSON:
+#        null
+#
+
+
+def schedule_download_submits(paper_ids: List[int], description: str, for_user: db.User, want_subdirs: bool, out_name: str):
     the_job = TheJob()
     job = the_job.create(db.JobType.download_submits, for_user)
     job.description = description
-    job.in_json = {'papers': paper_ids, 'want_subdirs': want_subdirs}
+    job.in_json = {'papers': paper_ids, 'want_subdirs': want_subdirs, 'out_name': out_name}
     the_job.submit()
 
 
 @job_handler(db.JobType.download_submits)
 def handle_download_submits(the_job: TheJob):
-    """Zazipuje zadané papíry.
-
-    Vstupní JSON:
-        { 'papers': [ seznam paper_id ke stažení ],
-          'want_feedback': true/false,
-        }
-
-    Výstupní JSON:
-        null
-    """
-
     job = the_job.job
     assert job.in_json is not None
     ids: List[int] = job.in_json['papers']  # type: ignore
     want_subdirs: bool = job.in_json['want_subdirs']  # type: ignore
+    out_name: str = job.in_json['out_name']  # type: ignore
 
     sess = db.get_session()
     papers = (sess.query(db.Paper, db.User, db.Task.code, db.Place)
@@ -57,11 +61,13 @@ def handle_download_submits(the_job: TheJob):
               .all())
     papers.sort(key=lambda p: (p[1].sort_key(), p[2]))
 
-    temp_file = NamedTemporaryFile(suffix='.zip', dir=data_dir('tmp'), mode='w+b')
-    logger.debug('Job: Vytvářím archiv %s', temp_file.name)
+    out_name = werkzeug.utils.secure_filename(out_name + '.zip')
+    out_path = job.file_path(out_name)
+    out_file = open(out_path, mode='w+b')
+    logger.debug('Job: Vytvářím archiv %s', out_path)
 
     cnt = 0
-    with zipfile.ZipFile(temp_file, mode='w') as zip:
+    with zipfile.ZipFile(out_file, mode='w') as zip:
         for p, u, task_code, place in papers:
             cnt += 1
             full_name = u.full_name()
@@ -76,10 +82,25 @@ def handle_download_submits(the_job: TheJob):
             zip.write(filename=os.path.join(data_dir('submits'), p.file_name or p.orig_file_name),
                       arcname=fn)
 
-    job.out_file = the_job.attach_file(temp_file.name, '.zip')
-    out_size = temp_file.tell()
+    job.out_file = out_name
+    out_size = out_file.tell()
     job.result = 'Celkem ' + inflect_number(cnt, 'soubor', 'soubory', 'souborů') + ', ' + data_size(out_size)
-    temp_file.close()
+    out_file.close()
+
+
+#
+# Job upload_feedback: Uploaduje opravená řešení
+#
+# Vstupní JSON:
+#        { 'round_id': <id>,
+#          'only_task_id': <id_or_null>,
+#          'only_contest_id': <id_or_null>,
+#          'only_site_id': <id_or_null>,
+#        }
+#
+# Výstupní JSON:
+#        null
+#
 
 
 def schedule_upload_feedback(round: db.Round, tmp_file: str, description: str, for_user: db.User,
@@ -97,7 +118,7 @@ def schedule_upload_feedback(round: db.Round, tmp_file: str, description: str, f
         'only_region_id': only_region.place_id if only_region is not None else None,
         'only_task_id': only_task.task_id if only_task is not None else None,
     }
-    job.in_file = the_job.attach_file(tmp_file, '.zip')
+    job.in_file = the_job.attach_file(tmp_file, 'upload.zip')
     the_job.submit()
 
 
@@ -148,19 +169,6 @@ def parse_feedback_name(name: str) -> Optional[UploadFeedback]:
 
 @job_handler(db.JobType.upload_feedback)
 def handle_upload_feedback(the_job: TheJob):
-    """Uploaduje opravená řešení.
-
-    Vstupní JSON:
-        { 'round_id': <id>,
-          'only_task_id': <id_or_null>,
-          'only_contest_id': <id_or_null>,
-          'only_site_id': <id_or_null>,
-        }
-
-    Výstupní JSON:
-        null
-    """
-
     job = the_job.job
     assert job.in_file is not None
     in_json = job.in_json
@@ -280,7 +288,7 @@ def handle_upload_feedback(the_job: TheJob):
             return False
 
     cnt_good = 0
-    parse_zip(job_file_path(job.in_file))
+    parse_zip(job.file_path(job.in_file))
 
     if not the_job.errors:
         resolve_tasks(files)
diff --git a/mo/tex/mo-logo.epdf b/mo/tex/mo-logo.epdf
new file mode 100644
index 0000000000000000000000000000000000000000..58a7625b5bf86a8d76e2b744ab40fa9b9015cb2d
Binary files /dev/null and b/mo/tex/mo-logo.epdf differ
diff --git a/mo/tex/protokol.tex b/mo/tex/protokol.tex
new file mode 100644
index 0000000000000000000000000000000000000000..0d494403ef04e7dbb60415efaad04dbfca8097f3
--- /dev/null
+++ b/mo/tex/protokol.tex
@@ -0,0 +1,119 @@
+\input ltluatex.tex
+\input luatex85.sty
+\input ucwmac2.tex
+
+\setmargins{15mm}
+\setuppage
+\nopagenumbers
+
+\ucwmodule{luaofs}
+\settextsize{12}
+\baselineskip=18pt
+
+\uselanguage{czech}
+\frenchspacing
+
+\newbox\logobox
+\setbox\logobox=\putimage{width 21mm}{mo-logo.epdf}
+
+\input qrcode.tex
+\qrset{height=23mm, level=H, tight, silent}
+\newbox\codebox
+
+\def\kolo{TODO}
+\def\kat{TODO}
+
+\newbox\ellipsisbox
+\setbox\ellipsisbox=\hbox{\bf~\dots~~}
+
+\directlua{
+	function cut_box(box_nr, max_w)
+		local box = tex.box[box_nr]
+		% nodetree.analyze(box)
+		local n
+		local total_w = 0
+		local last_visible
+		for n in node.traverse(box.head) do
+			local w, h, d = node.dimensions(n, n.next)
+			total_w = total_w + w
+			if total_w > max_w then
+				local new = node.copy_list(box.head, last_visible.next)
+				tex.box[box_nr] = node.hpack(new)
+				% nodetree.analyze(tex.box[box_nr])
+				return
+			end
+			if n.id == 0 or n.id == 2 or n.id == 29 then	% hlist, rule, glyph
+				last_visible = n
+			end
+		end
+	end
+}
+
+\def\limitedbox#1#2{%
+	\setbox0=\hbox{#2}%
+	\ifdim \wd0 > #1\relax
+		\dimen0=\dimexpr #1 - \wd\ellipsisbox\relax
+		\directlua{cut_box(0, tex.dimen[0])}%
+		\setbox0=\hbox{\box0\copy\ellipsisbox}%
+	\fi
+	\box0
+}
+
+\def\field#1#2{\hbox to #1{\limitedbox{#1}{#2}\hss}}
+\def\fillin#1{\smash{\lower 2pt\hbox to #1{\hrulefill}}}
+
+% \proto{kód}{jméno}{třída}{škola}{příklad}
+\def\proto#1#2#3#4#5{
+\setbox\codebox=\hbox{\qrcode{#1}}
+
+\line{%
+	\vhang{\copy\logobox}%
+	\qquad
+	\vhanglines{\baselineskip=14pt\vskip -5pt\hbox{\bf\kolo}\hbox{\bf\kat}}%
+	\hfil
+	\smash{\vhang{\box\codebox}}%
+}
+
+\medskip
+\prevdepth=0pt
+
+\leftline{%
+	\field{0.63\hsize}{Jméno: {\bf #2}}%
+	Třída: {\bf #3}%
+}
+\leftline{%
+	\field{0.63\hsize}{Škola: {\bf #4}}%
+	Příklad: {\bf #5}%
+}
+\leftline{%
+	\field{0.3\hsize}{List {\bf 1} ze \fillin{10mm}}%
+	\field{0.33\hsize}{Hodnotil:}%
+	Bodů:
+}
+
+\bigskip
+\hrule
+\vfill\eject
+}
+
+\def\universal{\proto{MO:*}{}{}{}{}}
+
+\def\blank{%
+\setbox\codebox=\hbox{\qrcode[height=15mm]{MO:+}}
+
+\line{%
+	\field{0.63\hsize}{Jméno:}%
+	\field{0.2\hsize}{Třída:}%
+	\hss
+	\raise\ht\strutbox\hbox{\smash{\vhang{\box\codebox}}}
+}
+\leftline{%
+	\field{0.63\hsize}{List \fillin{10mm} ze \fillin{10mm}}%
+	\field{0.2\hsize}{Příklad:}%
+}
+
+\bigskip
+\nointerlineskip
+\hbox to 0.85\hsize{\hrulefill}
+\vfill\eject
+}
diff --git a/mo/tex/qrcode.tex b/mo/tex/qrcode.tex
new file mode 100644
index 0000000000000000000000000000000000000000..4385efb3e23fa8212f8bb961bd34ab50c42fc6c3
--- /dev/null
+++ b/mo/tex/qrcode.tex
@@ -0,0 +1,2871 @@
+% qrcode.tex
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+% Petr Olsak <petr@olsak.net>                  Jul. 2015
+
+% This macro qrcode.tex is (roughly speaking) a copy of qrcode.sty
+% macro by Anders Hendrickson <ahendric@cord.edu>, see 
+% http://www.ctan.org/tex-archive/macros/latex/contrib/qrcode
+
+% The main difference between qrcode.sty and qrcode.tex is, that
+% the LaTeX ballast was removed from qrcode.sty by Petr Olsak. The result:
+% The qrcode.tex macro can be used in plain TeX format too.
+
+% Usage: after \input qrcode
+% you can type \qrcode{encoded text}.
+
+% More information about options can be found at the end of this file.
+
+\edef\tmp{\catcode`@=\the\catcode`@\relax}\catcode`\@=11 % LaTeX special character :(
+
+\newcount\qr@i
+\newcount\qr@j
+\newcount\qr@a
+\newcount\qr@b
+\newcount\qr@c
+
+\def\theqr@i{\the\qr@i}
+\def\theqr@j{\the\qr@j}
+
+\def\@relax{\relax}%
+
+\def\preface@macro#1#2{%
+  % #1 = macro name
+  % #2 = text to add to front of macro
+  \def\tempb{#2}%
+  \xa\xa\xa\def\xa\xa\xa#1\xa\xa\xa{\xa\tempb #1}%
+}%
+
+\def\g@preface@macro#1#2{%
+  % #1 = macro to be appended to
+  % #2 = code to add
+  \edef\codeA{#2}%
+  \expandafter\expandafter\expandafter
+  \gdef\expandafter\expandafter\expandafter#1\expandafter\expandafter\expandafter
+  {\expandafter\codeA#1}%
+}
+
+\def\qr@getstringlength#1{%
+  \bgroup
+    \qr@a=0%
+    \xdef\thestring{#1}%
+    \expandafter\qr@stringlength@recursive\expandafter(\thestring\relax\relax)%
+    \xdef\qr@stringlength{\the\qr@a}%
+  \egroup
+}%
+
+\def\qr@stringlength@recursive(#1#2){%
+  \def\testi{#1}%
+  \ifx\testi\@relax
+    %we are done.
+    \let\qr@next=\relax%
+  \else
+    \advance\qr@a by 1%
+    \def\qr@next{\qr@stringlength@recursive(#2)}%
+  \fi
+  \qr@next
+}%
+
+\def\qr@for#1=#2to#3by#4#{\forA{#1}{#2}{#3}{#4}}
+\long\def\forA#1#2#3#4#5{\begingroup
+   {\escapechar=`\\ % allocation of #1 as counter:
+      \expandafter \ifx\csname for:\string#1\endcsname \relax
+         \csname newcount\expandafter\endcsname \csname for:\string#1\endcsname\fi
+    \expandafter}\expandafter\let\expandafter#1\csname for:\string#1\endcsname
+   #1=#2%
+   \def\forB{#5\advance#1by#4\relax \expandafter\forC}%
+   \ifnum#4>0 \def\forC{\ifnum#1>#3\relax\else\forB\fi}%
+   \else      \def\forC{\ifnum#1<#3\relax\else\forB\fi}%
+   \fi
+   \ifnum#4=0 \let\forC=\relax \fi
+   \forC \endgroup
+}
+
+\def\qr@padatfront#1#2{%
+  % #1 = macro containing text to pad
+  % #2 = desired number of characters
+  % Pads a number with initial zeros.
+  \qr@getstringlength{#1}%
+  \qr@a=\qr@stringlength\relax%
+  \advance\qr@a by 1\relax%
+  \qr@for \i = \qr@a to #2 by 1
+    {\g@preface@macro{#1}{0}}%
+}
+
+\qr@a=-1\relax%
+\def\qr@savehexsymbols(#1#2){%
+  \advance\qr@a by 1\relax%
+  \expandafter\def\csname qr@hexchar@\the\qr@a\endcsname{#1}%
+  \expandafter\edef\csname qr@hextodecimal@#1\endcsname{\the\qr@a}%
+  \ifnum\qr@a=15\relax
+    %Done.
+    \let\qr@next=\relax%
+  \else
+    \def\qr@next{\qr@savehexsymbols(#2)}%
+  \fi%
+  \qr@next%
+}%
+\qr@savehexsymbols(0123456789abcdef\relax\relax)%
+
+\def\qr@decimaltobase#1#2#3{%
+  % #1 = macro to store result
+  % #2 = decimal representation of a positive integer
+  % #3 = new base
+  \bgroup
+    \edef\qr@newbase{#3}%
+    \gdef\qr@base@result{}%
+    \qr@a=#2\relax%
+    \qr@decimaltobase@recursive%
+    \xdef#1{\qr@base@result}%
+  \egroup
+}
+\def\qr@decimaltobase@recursive{%
+  \qr@b=\qr@a%
+  \divide\qr@b by \qr@newbase\relax
+  \multiply\qr@b by -\qr@newbase\relax
+  \advance\qr@b by \qr@a\relax%
+  \divide\qr@a by \qr@newbase\relax%
+  \ifnum\qr@b<10\relax
+    \edef\newdigit{\the\qr@b}%
+  \else
+    \edef\newdigit{\csname qr@hexchar@\the\qr@b\endcsname}%
+  \fi
+  \edef\qr@argument{{\noexpand\qr@base@result}{\newdigit}}%
+  \expandafter\g@preface@macro\qr@argument%
+  \ifnum\qr@a=0\relax
+    \relax
+  \else
+    \expandafter\qr@decimaltobase@recursive
+  \fi
+}
+
+\long\def\isnextchar#1#2#3{\begingroup\toks0={\endgroup#2}\toks1={\endgroup#3}%
+   \let\tmp=#1\futurelet\next\isnextcharA
+}
+\def\isnextcharA{\the\toks\ifx\tmp\next0\else1\fi\space}
+
+\long\def\xaddto#1#2{\expandafter\xdef\expandafter#1\expandafter{#1#2}}
+\let\g@addto@macro=\xaddto
+
+\def\qr@decimaltohex[#1]#2#3{%
+  % #1 (opt.) = number of hex digits to create
+  % #2 = macro to store result
+  % #3 = decimal digits to convert
+  \qr@decimaltobase{#2}{#3}{16}%
+  \qr@padatfront{#2}{#1}%
+}
+
+\def\qr@decimaltobinary[#1]#2#3{%
+  % #1 (opt.) = number of bits to create
+  % #2 = macro to store result
+  % #3 = decimal digits to convert
+  \qr@decimaltobase{#2}{#3}{2}%
+  \qr@padatfront{#2}{#1}%
+}
+
+\qr@for \i = 0 to 15 by 1%
+  {%
+   \qr@decimaltohex[1]{\qr@hexchar}{\the\i}%
+   \qr@decimaltobinary[4]{\qr@bits}{\the\i}%
+   \expandafter\xdef\csname qr@b2h@\qr@bits\endcsname{\qr@hexchar}%
+   \expandafter\xdef\csname qr@h2b@\qr@hexchar\endcsname{\qr@bits}%
+  }%
+
+\def\qr@binarytohex[#1]#2#3{%
+  % #1 (optional) = # digits desired
+  % #2 = macro to save to
+  % #3 = binary string (must be multiple of 4 bits)
+  \def\test@i{#1}%
+  \ifx\test@i\@relax%
+    %No argument specified
+    \def\qr@desireddigits{0}%
+  \else
+    \def\qr@desireddigits{#1}%
+  \fi
+  \gdef\qr@base@result{}%
+  \edef\qr@argument{(#3\relax\relax\relax\relax\relax)}%
+  \xa\qr@binarytohex@int\qr@argument%
+  \qr@padatfront{\qr@base@result}{\qr@desireddigits}%
+  \xdef#2{\qr@base@result}%
+}
+\def\qr@binarytohex@int(#1#2#3#4#5){%
+  % #1#2#3#4 = 4 bits
+  % #5 = remainder, including \relax\relax\relax\relax\relax terminator
+  \def\test@i{#1}%
+  \ifx\test@i\@relax%
+    %Done.
+    \def\qr@next{\relax}%
+  \else%
+    \xdef\qr@base@result{\qr@base@result\csname qr@b2h@#1#2#3#4\endcsname}%
+    \def\qr@next{\qr@binarytohex@int(#5)}%
+  \fi%
+  \qr@next%
+}
+
+\def\qr@hextobinary[#1]#2#3{%
+  % #1 (optional) = # bits desired
+  % #2 = macro to save to
+  % #3 = hexadecimal string
+  \bgroup
+  \def\test@i{#1}%
+  \ifx\test@i\@relax%
+    %No argument specified
+    \def\qr@desireddigits{0}%
+  \else
+    \def\qr@desireddigits{#1}%
+  \fi
+  \gdef\qr@base@result{}%
+  \edef\qr@argument{(#3\relax\relax)}%
+  \xa\qr@hextobinary@int\qr@argument%
+  \qr@padatfront{\qr@base@result}{\qr@desireddigits}%
+  \xdef#2{\qr@base@result}%
+  \egroup
+}
+\def\qr@hextobinary@int(#1#2){%
+  % #1 = hexadecimal character
+  % #2 = remainder, including \relax\relax terminator
+  \def\test@@i{#1}%
+  \ifx\test@@i\@relax%
+    %Done.
+    \def\qr@next{\relax}%
+  \else%
+    \xdef\qr@base@result{\qr@base@result\csname qr@h2b@#1\endcsname}%
+    \def\qr@next{\qr@hextobinary@int(#2)}%
+  \fi%
+  \qr@next%
+}
+
+\def\qr@hextodecimal#1#2{%
+  \edef\qr@argument{#2}%
+  \xa\qr@a\xa=\xa\number\xa"\qr@argument\relax%
+  \edef#1{\the\qr@a}%
+}
+
+\def\qr@hextodecimal#1#2{%
+  % #1 = macro to store result
+  % #2 = hexadecimal representation of a positive integer
+  \bgroup
+    \qr@a=0\relax%
+    \edef\qr@argument{(#2\relax)}%
+    \xa\qr@hextodecimal@recursive\qr@argument%
+    \xdef#1{\the\qr@a}%
+  \egroup
+}
+\def\qr@hextodecimal@recursive(#1#2){%
+  % #1 = first hex char
+  % #2 = remainder
+  \advance \qr@a by \csname qr@hextodecimal@#1\endcsname\relax%
+  \edef\testii{#2}%
+  \ifx\testii\@relax%
+    %Done.
+    \let\qr@next=\relax%
+  \else
+    %There's at least one more digit.
+    \multiply\qr@a by 16\relax
+    \edef\qr@next{\noexpand\qr@hextodecimal@recursive(#2)}%
+  \fi%
+  \qr@next%
+}
+
+\def\qrverbatim{\def\do##1{\catcode`##1=12}\dospecials
+  \catcode`\\=0 \catcode`\{=1 \catcode`\}=2
+  \escapechar=-1 \def\do##1{\edef##1{\string##1}}\dospecials
+  \def\?{^^J}\let\ =\qr@letterspace
+  \catcode`\^^M=13 \qr@setMtoJ
+  \ifx\mubytein\undefined \else \mubytein=0 \fi
+}
+{\lccode`\?=`\ \lowercase{\gdef\qr@letterspace{?}}}
+{\catcode`\^^M=13 \gdef\qr@setMtoJ{\def^^M{^^J}}}
+
+\def\qr@creatematrix#1{%
+  \expandafter\gdef\csname #1\endcsname##1##2{%
+    \csname #1@##1@##2\endcsname
+  }%
+}%
+
+\def\qr@storetomatrix#1#2#3#4{%
+  % #1 = matrix name
+  % #2 = row number
+  % #3 = column number
+  % #4 = value of matrix entry
+  \xa\gdef\csname #1@#2@#3\endcsname{#4}%
+}%
+
+\def\qr@estoretomatrix#1#2#3#4{%
+  % This version performs exactly one expansion on #4.
+  % #1 = matrix name
+  % #2 = row number
+  % #3 = column number
+  % #4 = value of matrix
+  \expandafter\gdef\csname #1@#2@#3\expandafter\endcsname\expandafter{#4}%
+}%
+
+\def\qr@matrixentry#1#2#3{%
+  % #1 = matrix name
+  % #2 = row number
+  % #3 = column number
+  \csname #1@#2@#3\endcsname%
+}%
+
+\def\qr@createsquareblankmatrix#1#2{%
+  \qr@creatematrix{#1}%
+  \xa\gdef\csname #1@numrows\endcsname{#2}%
+  \xa\gdef\csname #1@numcols\endcsname{#2}%
+  \qr@for \i = 1 to #2 by 1%
+    {\qr@for \j = 1 to #2 by 1%
+      {\qr@storetomatrix{#1}{\the\i}{\the\j}{\@blank}}}%
+}%
+
+\def\qr@numberofrowsinmatrix#1{%
+  \csname #1@numrows\endcsname%
+}%
+
+\def\qr@numberofcolsinmatrix#1{%
+  \csname #1@numcols\endcsname%
+}%
+
+\def\qr@setnumberofrows#1#2{%
+  \xa\xdef\csname #1@numrows\endcsname{#2}%
+}%
+
+\def\qr@setnumberofcols#1#2{%
+  \xa\xdef\csname #1@numcols\endcsname{#2}%
+}%
+
+\newdimen\qrdesiredheight
+\newdimen\qrmodulesize
+
+\def\qr@link#1#2{\hbox{\pdfstartlink height\ht0 depth0pt \qr@border
+   user{/Subtype/Link/A <</Type/Action/S/URI/URI(#1)>>}\relax #2\pdfendlink}%
+}
+\def\qr@border{\expandafter\ifx \csname kv:qrborder\endcsname\relax \else
+   attr{/C[\kv{qrborder}] /Border[0 0 .6]}\fi
+}
+
+\def\qr@createliteralmatrix#1#2#3{%
+  % #1 = matrix name
+  % #2 = m, the number of rows and columns in the square matrix
+  % #3 = a string of m^2 tokens to be written into the matrix
+  \qr@creatematrix{#1}%
+  \expandafter\xdef\csname #1@numrows\endcsname{#2}%
+  \expandafter\xdef\csname #1@numcols\endcsname{#2}%
+  \gdef\qr@literalmatrix@tokens{#3}%
+  \qr@for \i = 1 to #2 by 1%
+    {\qr@for \j = 1 to #2 by 1%
+      {\expandafter\qr@createliteralmatrix@int\expandafter(\qr@literalmatrix@tokens)%
+       \qr@estoretomatrix{#1}{\the\i}{\the\j}{\qr@entrytext}%
+      }%
+    }%
+}
+\def\qr@createliteralmatrix@int(#1#2){%
+  \def\qr@entrytext{#1}%
+  \gdef\qr@literalmatrix@tokens{#2}%
+}
+
+\qr@createliteralmatrix{finderpattern}{8}{%
+  \qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@white@fixed%
+  \qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed%
+}%
+
+\qr@createliteralmatrix{alignmentpattern}{5}{%
+  \qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@black@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@black@fixed\qr@white@fixed\qr@black@fixed%
+  \qr@black@fixed\qr@white@fixed\qr@white@fixed\qr@white@fixed\qr@black@fixed%
+  \qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed\qr@black@fixed%
+}%
+
+\def\qr@copymatrixentry#1#2#3#4#5#6{%
+  % Copy the (#2,#3) entry of matrix #1
+  % to the (#5,#6) position of matrix #4.
+  \xa\xa\xa\global%
+  \xa\xa\xa\let\xa\xa\csname #4@#5@#6\endcsname%
+                     \csname #1@#2@#3\endcsname%
+}%
+
+\def\qr@createduplicatematrix#1#2{%
+  % #1 = name of copy
+  % #2 = original matrix to be copied
+  \qr@creatematrix{#1}%
+  \qr@for \i = 1 to \qr@numberofrowsinmatrix{#2} by 1%
+    {\qr@for \j = 1 to \qr@numberofcolsinmatrix{#2} by 1%
+      {\qr@copymatrixentry{#2}{\the\i}{\the\j}{#1}{\the\i}{\the\j}%
+      }%
+    }%
+  \qr@setnumberofrows{#1}{\qr@numberofrowsinmatrix{#2}}%
+  \qr@setnumberofcols{#1}{\qr@numberofcolsinmatrix{#2}}%
+}%
+
+\def\qr@placefinderpattern@int#1#2#3#4#5{%
+  % Work on matrix #1.
+  % Start in position (#2, #3) -- should be a corner
+  % #4 indicates horizontal direction (1=right, -1=left)
+  % #5 indicates vertical direction (1=down, -1=up)
+  %
+  % In this code, \sourcei and \sourcej are TeX counts working through the finderpattern matrix,
+  % and i and j are LaTeX counters indicating positions in the big matrix.
+  \setcounter{qr@i}{#2}%
+  \qr@for \sourcei=1 to 8 by 1%
+    {\setcounter{qr@j}{#3}%
+     \qr@for \sourcej=1 to 8 by 1%
+       {\qr@copymatrixentry{finderpattern}{\the\sourcei}{\the\sourcej}%
+                        {#1}{\theqr@i}{\theqr@j}%
+        \addtocounter{qr@j}{#5}%
+       }%
+     \addtocounter{qr@i}{#4}%
+    }%
+}%
+
+\def\qr@placefinderpatterns#1{%
+  % #1=matrix name
+  \qr@placefinderpattern@int{#1}{1}{1}{1}{1}%
+  \qr@placefinderpattern@int{#1}{\qr@numberofrowsinmatrix{#1}}{1}{-1}{1}%
+  \qr@placefinderpattern@int{#1}{1}{\qr@numberofcolsinmatrix{#1}}{1}{-1}%
+}%
+
+\def\qr@placetimingpatterns#1{%
+  %Set \endingcol to n-8.
+  \qr@a=\qr@size\relax%
+  \advance\qr@a by -8\relax%
+  \edef\endingcol{\the\qr@a}%
+  \qr@for \j = 9 to \endingcol by 1%
+    {\ifodd\j\relax%
+       \qr@storetomatrix{#1}{7}{\the\j}{\qr@black@fixed}%
+       \qr@storetomatrix{#1}{\the\j}{7}{\qr@black@fixed}%
+     \else%
+       \qr@storetomatrix{#1}{7}{\the\j}{\qr@white@fixed}%
+       \qr@storetomatrix{#1}{\the\j}{7}{\qr@white@fixed}%
+     \fi%
+    }%
+}%
+
+\def\qr@placealignmentpattern@int#1#2#3{%
+  % Work on matrix #1.
+  % Write an alignment pattern into the matrix, centered on (#2,#3).
+  \qr@a=#2\relax%
+  \advance\qr@a by -2\relax%
+  \qr@b=#3\relax%
+  \advance\qr@b by -2\relax%
+  \setcounter{qr@i}{\the\qr@a}%
+  \qr@for \i=1 to 5 by 1%
+    {\setcounter{qr@j}{\the\qr@b}%
+     \qr@for \j=1 to 5 by 1%
+      {\qr@copymatrixentry{alignmentpattern}{\the\i}{\the\j}%
+                       {#1}{\theqr@i}{\theqr@j}%
+       \stepcounter{qr@j}%
+      }%
+     \stepcounter{qr@i}%
+    }%
+}%
+
+\newif\ifqr@incorner%
+\def\qr@placealignmentpatterns#1{%
+  %There are k^2-3 alignment patterns,
+  %arranged in a (k x k) grid within the matrix.
+  %They begin in row 7, column 7,
+  %except that the ones in the NW, NE, and SW corners
+  %are omitted because of the finder patterns.
+  %Recall that
+  %  * \qr@k stores k,
+  %  * \qr@alignment@firstskip stores how far between the 1st and 2nd row/col, &
+  %  * \qr@alignment@generalskip stores how far between each subsequent row/col.
+  \xa\ifnum\qr@k>0\relax
+    %There will be at least one alignment pattern.
+    %N.B. k cannot equal 1.
+    \xa\ifnum\qr@k=2\relax
+      % 2*2-3 = exactly 1 alignment pattern.
+      \qr@a=7\relax
+      \advance\qr@a by \qr@alignment@firstskip\relax
+      \xdef\qr@target@ii{\the\qr@a}%
+      \qr@placealignmentpattern@int{#1}{\qr@target@ii}{\qr@target@ii}%
+    \else
+      % k is at least 3, so the following loops should be safe.
+      \xdef\qr@target@ii{7}%
+      \qr@for \ii = 1 to \qr@k by 1%
+        {\ifcase\ii\relax%
+           \relax% \ii should never equal 0.
+         \or
+           \xdef\qr@target@ii{7}% If \ii = 1, we start in row 7.
+         \or
+           %If \ii = 2, we add the firstskip.
+           \qr@a=\qr@target@ii\relax%
+           \advance\qr@a by \qr@alignment@firstskip\relax%
+           \xdef\qr@target@ii{\the\qr@a}%
+         \else
+           %If \ii>2, we add the generalskip.
+           \qr@a=\qr@target@ii\relax%
+           \advance\qr@a by \qr@alignment@generalskip\relax%
+           \xdef\qr@target@ii{\the\qr@a}%
+         \fi
+         \qr@for \jj = 1 to \qr@k by 1%
+           {\ifcase\jj\relax%
+              \relax% \jj should never equal 0.
+            \or
+              \xdef\qr@target@jj{7}% If \jj=1, we start in row 7.
+            \or
+              %If \jj=2, we add the firstskip.
+              \qr@a=\qr@target@jj\relax%
+              \advance\qr@a by \qr@alignment@firstskip%
+              \xdef\qr@target@jj{\the\qr@a}%
+            \else
+              %If \jj>2, we add the generalskip.
+              \qr@a=\qr@target@jj\relax%
+              \advance\qr@a by \qr@alignment@generalskip%
+              \xdef\qr@target@jj{\the\qr@a}%
+            \fi
+            \qr@incornerfalse%
+            \ifnum\ii=1\relax
+              \ifnum\jj=1\relax
+                \qr@incornertrue
+              \else
+                \ifnum\qr@k=\jj\relax
+                  \qr@incornertrue
+                \fi
+              \fi
+            \else
+              \xa\ifnum\qr@k=\ii\relax
+                \ifnum\jj=1\relax
+                  \qr@incornertrue
+                \fi
+              \fi
+            \fi
+            \ifqr@incorner
+              \relax
+            \else
+              \qr@placealignmentpattern@int{#1}{\qr@target@ii}{\qr@target@jj}%
+            \fi
+           }% ends \qr@for \jj
+        }% ends \qr@for \ii
+    \fi
+  \fi
+}%
+
+\def\qr@placedummyformatpatterns#1{%
+  \qr@for \j = 1 to 9 by 1%
+    {\ifnum\j=7\relax%
+     \else%
+       \qr@storetomatrix{#1}{9}{\the\j}{\qr@format@square}%
+       \qr@storetomatrix{#1}{\the\j}{9}{\qr@format@square}%
+     \fi%
+    }%
+  \setcounter{qr@j}{\qr@size}%
+  \qr@for \j = 1 to 8 by 1%
+    {\qr@storetomatrix{#1}{9}{\theqr@j}{\qr@format@square}%
+     \qr@storetomatrix{#1}{\theqr@j}{9}{\qr@format@square}%
+     \addtocounter{qr@j}{-1}%
+    }%
+  %Now go back and change the \qr@format@square in (n-8,9) to \qr@black@fixed.
+  \addtocounter{qr@j}{1}%
+  \qr@storetomatrix{#1}{\theqr@j}{9}{\qr@black@fixed}%
+}%
+
+\def\qr@placedummyversionpatterns#1{%
+  \xa\ifnum\qr@version>6\relax
+    %Must include version information.
+    \global\qr@i=\qr@size%
+    \global\advance\qr@i by -10\relax%
+    \qr@for \i = 1 to 3 by 1%
+      {\qr@for \j = 1 to 6 by 1%
+        {\qr@storetomatrix{#1}{\theqr@i}{\the\j}{\qr@format@square}%
+         \qr@storetomatrix{#1}{\the\j}{\theqr@i}{\qr@format@square}%
+        }%
+       \stepcounter{qr@i}%
+      }%
+  \fi
+}%
+
+\def\qr@writebit(#1#2)#3{%
+  % #3 = matrix name
+  % (qr@i,qr@j) = position to write in (LaTeX counters)
+  % #1 = bit to be written
+  % #2 = remaining bits plus '\relax' as an end-of-file marker
+  \edef\qr@datatowrite{#2}%
+  \ifnum#1=1
+    \qr@storetomatrix{#3}{\theqr@i}{\theqr@j}{\qr@black}%
+  \else
+    \qr@storetomatrix{#3}{\theqr@i}{\theqr@j}{\@white}%
+  \fi
+}%
+
+\newif\ifqr@rightcol
+\newif\ifqr@goingup
+
+\def\qr@writedata@hex#1#2{%
+  % #1 = name of a matrix that has been prepared with finder patterns, timing patterns, etc.
+  % #2 = a string consisting of bytes to write into the matrix, in two-char hex format.
+  \setcounter{qr@i}{\qr@numberofrowsinmatrix{#1}}%
+  \setcounter{qr@j}{\qr@numberofcolsinmatrix{#1}}%
+  \qr@rightcoltrue%
+  \qr@goinguptrue%
+  \edef\qr@argument{{#1}(#2\relax\relax\relax)}%
+  \xa\qr@writedata@hex@recursive\qr@argument%
+}%
+
+\def\qr@writedata@hex@recursive#1(#2#3#4){%
+  % #1 = name of a matrix that has been prepared with finder patterns, timing patterns, etc.
+  % (qr@i,qr@j) = position to write in LaTeX counters
+  % #2#3#4 contains the hex codes of the bytes to be written, plus \relax\relax\relax
+  % as an end-of-file marker
+  \edef\testii{#2}%
+  \ifx\testii\@relax%
+    % #2 is \relax, so there is nothing more to write.
+    \relax
+    \let\go=\relax
+  \else
+    % #2 is not \relax, so there is another byte to write.
+    \qr@hextobinary[8]{\bytetowrite}{#2#3}%
+    \xdef\qr@datatowrite{\bytetowrite\relax}% %Add terminating "\relax"
+    \qr@writedata@recursive{#1}% %This function actually writes the 8 bits.
+    \edef\qr@argument{{#1}(#4)}%
+    \xa\def\xa\go\xa{\xa\qr@writedata@hex@recursive\qr@argument}% %Call self to write the next bit.
+  \fi
+  \go
+}%
+
+\def\qr@writedata#1#2{%
+  % #1 = name of a matrix that has been prepared with finder patterns, timing patterns, etc.
+  % #2 = a string consisting of 0's and 1's to write into the matrix.
+  \setcounter{qr@i}{\qr@numberofrowsinmatrix{#1}}%
+  \setcounter{qr@j}{\qr@numberofcolsinmatrix{#1}}%
+  \qr@rightcoltrue
+  \qr@goinguptrue
+  \edef\qr@datatowrite{#2\relax}%
+  \qr@writedata@recursive{#1}%
+}%
+
+\def\@@blank{\@blank}%
+
+\def\qr@writedata@recursive#1{%
+  % #1 = matrix name
+  % (qr@i,qr@j) = position to write in (LaTeX counters)
+  % \qr@datatowrite contains the bits to be written, plus '\relax' as an end-of-file marker
+  \xa\let\xa\squarevalue\csname #1@\theqr@i @\theqr@j\endcsname%
+  \ifx\squarevalue\@@blank
+    %Square is blank, so write data in it.
+    \xa\qr@writebit\xa(\qr@datatowrite){#1}%
+    %The \qr@writebit macro not only writes the first bit of \qr@datatowrite into the matrix,
+    %but also removes the bit from the 'bitstream' of \qr@datatowrite.
+  \fi
+  %Now adjust our position in the matrix.
+  \ifqr@rightcol
+    %From the right-hand half of the two-bit column, we always move left.  Easy peasy.
+    \addtocounter{qr@j}{-1}%
+    \qr@rightcolfalse
+  \else
+    %If we're in the left-hand column, things are harder.
+    \ifqr@goingup
+      %First, suppose we're going upwards.
+      \ifnum\qr@i>1\relax%
+        %If we're not in the first row, things are easy.
+        %We move one to the right and one up.
+        \addtocounter{qr@j}{1}%
+        \addtocounter{qr@i}{-1}%
+        \qr@rightcoltrue
+      \else
+        %If we are in the first row, then we move to the left,
+        %and we are now in the right-hand column on a downward pass.
+        \addtocounter{qr@j}{-1}%
+        \qr@goingupfalse
+        \qr@rightcoltrue
+      \fi
+    \else
+      %Now, suppose we're going downwards.
+      \xa\ifnum\qr@size>\qr@i\relax%
+        %If we're not yet in the bottom row, things are easy.
+        %We move one to the right and one down.
+        \addtocounter{qr@j}{1}%
+        \addtocounter{qr@i}{1}%
+        \qr@rightcoltrue
+      \else
+        %If we are in the bottom row, then we move to the left,
+        %and we are now in the right-hand column on an upward pass.
+        \addtocounter{qr@j}{-1}%
+        \qr@rightcoltrue
+        \qr@goinguptrue
+      \fi
+    \fi
+    %One problem: what if we just moved into the 7th column?
+    %Das ist verboten.
+    %If we just moved (left) into the 7th column, we should move on into the 6th column.
+    \ifnum\qr@j=7\relax%
+      \setcounter{qr@j}{6}%
+    \fi
+  \fi
+  %Now check whether there are any more bits to write.
+  \ifx\qr@datatowrite\@relax
+    % \qr@datatowrite is just `\relax', so we're done.
+    \let\nexttoken=\relax
+    \relax
+  \else
+    % Write some more!
+    \def\nexttoken{\qr@writedata@recursive{#1}}%
+  \fi
+  \nexttoken
+}%
+
+\def\qr@writeremainderbits#1{%
+  % #1 = name of a matrix that has been prepared and partly filled.
+  % (qr@i,qr@j) = position to write in LaTeX counters
+  \xa\ifnum\qr@numremainderbits>0\relax
+    \def\qr@datatowrite{}%
+    \qr@for \i = 1 to \qr@numremainderbits by 1%
+      {\g@addto@macro{\qr@datatowrite}{0}}%
+    \g@addto@macro{\qr@datatowrite}{\relax}% terminator
+    \qr@writedata@recursive{#1}%
+  \fi
+}%
+
+\newif\ifqr@cellinmask
+
+\def\qr@setmaskingfunction#1{%
+  % #1 = 1 decimal digit for the mask. (I see no reason to use the 3-bit binary code.)
+  % The current position is (\themaski,\themaskj), with indexing starting at 0.
+  \edef\maskselection{#1}%
+  \xa\ifcase\maskselection\relax
+    %Case 0: checkerboard
+    \def\qr@parsemaskingfunction{%
+      % Compute mod(\themaski+\themaskj,2)%
+      \qr@a=\maski%
+      \advance\qr@a by \maskj%
+      \qr@b=\qr@a%
+      \divide\qr@b by 2%
+      \multiply\qr@b by 2%
+      \advance\qr@a by -\qr@b%
+      \edef\qr@maskfunctionresult{\the\qr@a}%
+    }%
+  \or
+    %Case 1: horizontal stripes
+    \def\qr@parsemaskingfunction{%
+      % Compute mod(\themaski,2)%
+      \ifodd\maski\relax%
+        \def\qr@maskfunctionresult{1}%
+      \else%
+        \def\qr@maskfunctionresult{0}%
+      \fi%
+    }%
+  \or
+    %Case 2: vertical stripes
+    \def\qr@parsemaskingfunction{%
+      % Compute mod(\themaskj,3)%
+      \qr@a=\maskj%
+      \divide\qr@a by 3%
+      \multiply\qr@a by 3%
+      \advance\qr@a by -\maskj%
+      \edef\qr@maskfunctionresult{\the\qr@a}%
+    }%
+  \or
+    %Case 3: diagonal stripes
+    \def\qr@parsemaskingfunction{%
+      % Compute mod(\themaski+\themaskj,3)%
+      \qr@a=\maski%
+      \advance\qr@a by \maskj%
+      \qr@b=\qr@a%
+      \divide\qr@b by 3%
+      \multiply\qr@b by 3%
+      \advance\qr@b by -\qr@a%
+      \edef\qr@maskfunctionresult{\the\qr@b}%
+    }%
+  \or
+    %Case 4: wide checkerboard
+    \def\qr@parsemaskingfunction{%
+      % Compute mod(floor(\themaski/2) + floor(\themaskj/3),2) %
+      \qr@a=\maski%
+      \divide\qr@a by 2%
+      \qr@b=\maskj%
+      \divide\qr@b by 3%
+      \advance\qr@a by \qr@b%
+      \qr@b=\qr@a%
+      \divide\qr@a by 2%
+      \multiply\qr@a by 2%
+      \advance\qr@a by -\qr@b%
+      \edef\qr@maskfunctionresult{\the\qr@a}%
+    }%
+  \or
+    %Case 5: quilt
+    \def\qr@parsemaskingfunction{%
+      % Compute mod(\themaski*\themaskj,2) + mod(\themaski*\themaskj,3) %
+      \qr@a=\maski%
+      \multiply\qr@a by \maskj%
+      \qr@b=\qr@a%
+      \qr@c=\qr@a%
+      \divide\qr@a by 2%
+      \multiply\qr@a by 2%
+      \advance\qr@a by -\qr@c% (result will be -mod(i*j,2), which is negative.)
+      \divide\qr@b by 3%
+      \multiply\qr@b by 3%
+      \advance\qr@b by -\qr@c% (result will be -mod(i*j,3), which is negative.)
+      \advance\qr@a by \qr@b% (result is negative of what's in the spec.)
+      \edef\qr@maskfunctionresult{\the\qr@a}%
+    }%
+  \or
+    %Case 6: arrows
+    \def\qr@parsemaskingfunction{%
+      % Compute mod( mod(\themaski*\themaskj,2) + mod(\themaski*\themaskj,3) , 2 ) %
+      \qr@a=\maski%
+      \multiply\qr@a by \maskj%
+      \qr@b=\qr@a%
+      \qr@c=\qr@a%
+      \multiply\qr@c by 2% % \qr@c equals 2*i*j.
+      \divide\qr@a by 2%
+      \multiply\qr@a by 2%
+      \advance\qr@c by -\qr@a% Now \qr@c equals i*j + mod(i*j,2).
+      \divide\qr@b by 3%
+      \multiply\qr@b by 3%
+      \advance\qr@c by -\qr@b% (Now \qr@c equals mod(i*j,2) + mod(i*j,3).
+      \qr@a=\qr@c%
+      \divide\qr@a by 2%
+      \multiply\qr@a by 2%
+      \advance\qr@c by-\qr@a%
+      \edef\qr@maskfunctionresult{\the\qr@c}%
+    }%
+  \or
+    %Case 7: shotgun
+    \def\qr@parsemaskingfunction{%
+      % Compute mod( mod(\themaski+\themaskj,2) + mod(\themaski*\themaskj,3) , 2 ) %
+      \qr@a=\maski%
+      \advance\qr@a by \maskj% %So \qr@a = i+j
+      \qr@b=\maski%
+      \multiply\qr@b by \maskj% %So \qr@b = i*j
+      \qr@c=\qr@a%
+      \advance\qr@c by \qr@b% So \qr@c = i+j+i*j
+      \divide\qr@a by 2%
+      \multiply\qr@a by 2%
+      \advance\qr@c by -\qr@a% So \qr@c = mod(i+j,2) + i*j
+      \divide\qr@b by 3%
+      \multiply\qr@b by 3%
+      \advance\qr@c by -\qr@b% So \qr@c = mod(i+j,2) + mod(i*j,3)
+      \qr@a=\qr@c%
+      \divide\qr@c by 2%
+      \multiply\qr@c by 2%
+      \advance\qr@a by -\qr@c%
+      \edef\qr@maskfunctionresult{\the\qr@a}%
+    }%
+  \fi
+}%
+
+\def\qr@checkifcellisinmask{%
+  % The current position is (\i,\j), in TeX counts,
+  % but the LaTeX counters (maski,maskj) should contain
+  % the current position with indexing starting at 0.
+  % That is, maski = \i-1 and maskj = \j-1.
+  %
+  % \qr@parsemaskingfunction must have been set by a call to \qr@setmaskingfunction
+  \qr@parsemaskingfunction
+  \xa\ifnum\qr@maskfunctionresult=0\relax
+    \qr@cellinmasktrue
+  \else
+    \qr@cellinmaskfalse
+  \fi
+}%
+
+\newcount\maski
+\newcount\maskj
+
+\def\qr@applymask#1#2#3{%
+  % #1 = name of a matrix that should be filled out completely
+  %      except for the format and/or version information.
+  % #2 = name of a new matrix to contain the masked version
+  % #3 = 1 decimal digit naming the mask
+  \qr@createduplicatematrix{#2}{#1}%
+  \qr@setmaskingfunction{#3}%
+  \setcounter{maski}{-1}%
+  \qr@for \i = 1 to \qr@size by 1%
+    {\stepcounter{maski}%
+     \setcounter{maskj}{-1}%
+     \qr@for \j = 1 to \qr@size by 1%
+     {\stepcounter{maskj}%
+      \qr@checkifcellisinmask
+      \ifqr@cellinmask
+        \qr@checkifcurrentcellcontainsdata{#2}%
+        \ifqr@currentcellcontainsdata
+          \qr@flipcurrentcell{#2}%
+        \fi
+      \fi
+      }%
+    }%
+}%
+
+\newif\ifqr@currentcellcontainsdata
+\qr@currentcellcontainsdatafalse
+
+\def\@@white{\@white}%
+\def\@@black{\qr@black}%
+
+\def\qr@checkifcurrentcellcontainsdata#1{%
+  % #1 = name of matrix
+  \qr@currentcellcontainsdatafalse
+  \xa\ifx\csname #1@\the\i @\the\j\endcsname\@@white
+    \qr@currentcellcontainsdatatrue
+  \fi
+  \xa\ifx\csname #1@\the\i @\the\j\endcsname\@@black
+    \qr@currentcellcontainsdatatrue
+  \fi
+}%
+
+\def\qr@flipped@black{\qr@black}%
+\def\qr@flipped@white{\@white}%
+
+\def\qr@flipcurrentcell#1{%
+  % #1 = name of matrix
+  % (\i, \j) = current position, in TeX counts.
+  % This assumes the cell contains data, either black or white!
+  \xa\ifx\csname #1@\the\i @\the\j\endcsname\@@white
+    \qr@storetomatrix{#1}{\the\i}{\the\j}{\qr@flipped@black}%
+  \else
+    \qr@storetomatrix{#1}{\the\i}{\the\j}{\qr@flipped@white}%
+  \fi
+}%
+
+\def\qr@chooseandapplybestmask#1{%
+  % #1 = name of a matrix that should be filled out completely
+  %      except for the format and/or version information.
+  % This function applies all eight masks in succession,
+  % calculates their penalties, and remembers the best.
+  % The number indicating which mask was used is saved in \qr@mask@selected.
+  \qr@createduplicatematrix{originalmatrix}{#1}%
+  \qrmessage{<Applying Mask 0...}%
+  \qr@applymask{originalmatrix}{#1}{0}%
+  \qrmessage{done. Calculating penalty...}%
+  \qr@evaluatemaskpenalty{#1}%
+  \xdef\currentbestpenalty{\qr@penalty}%
+  \qrmessage{penalty is \qr@penalty>^^J}%
+  \gdef\currentbestmask{0}%
+  \qr@for \i = 1 to 7 by 1%
+    {\qrmessage{<Applying Mask \the\i...}%
+     \qr@applymask{originalmatrix}{currentmasked}{\the\i}%
+     \qrmessage{done. Calculating penalty...}%
+     \qr@evaluatemaskpenalty{currentmasked}%
+     \qrmessage{penalty is \qr@penalty>^^J}%
+     \xa\xa\xa\ifnum\xa\qr@penalty\xa<\currentbestpenalty\relax
+       %We found a better mask.
+       \xdef\currentbestmask{\the\i}%
+       \qr@createduplicatematrix{#1}{currentmasked}%
+       \xdef\currentbestpenalty{\qr@penalty}%
+     \fi
+    }%
+  \xdef\qr@mask@selected{\currentbestmask}%
+  \qrmessage{<Selected Mask \qr@mask@selected>^^J}%
+}%
+
+\def\qr@Ni{3}%
+\def\qr@Nii{3}%
+\def\qr@Niii{40}%
+\def\qr@Niv{10}%
+\def\@fiveones{11111}%
+\def\@fivezeros{11111}%
+\def\@twoones{11}%
+\def\@twozeros{00}%
+\def\@finderA{00001011101}%
+\def\@finderB{10111010000}%
+\def\@finderB@three{1011101000}%
+\def\@finderB@two{101110100}%
+\def\@finderB@one{10111010}%
+\def\@finderB@zero{1011101}%
+\newif\ifstringoffive
+\def\addpenaltyiii{%
+  \addtocounter{penaltyiii}{\qr@Niii}%
+}%
+\newcount\totalones
+\newcount\penaltyi
+\newcount\penaltyii
+\newcount\penaltyiii
+\newcount\penaltyiv
+\def\qr@evaluatemaskpenalty#1{%
+  % #1 = name of a matrix that we will test for the penalty
+  % according to the specs.
+  \setcounter{penaltyi}{0}%
+  \setcounter{penaltyii}{0}%
+  \setcounter{penaltyiii}{0}%
+  \setcounter{penaltyiv}{0}%
+  \bgroup%localize the meanings we give to the symbols
+    \def\qr@black{1}\def\@white{0}%
+    \def\qr@black@fixed{1}\def\qr@white@fixed{0}%
+    \def\qr@format@square{0}% This is not stated in the specs, but seems
+                            % to be the standard implementation.
+    \def\@blank{0}% These would be any bits at the end.
+    %
+    \setcounter{totalones}{0}%
+    \qr@for \i=1 to \qr@size by 1%
+      {\def\lastfive{z}% %The z is a dummy, that will be removed before any testing.
+       \stringoffivefalse
+       \def\lasttwo@thisrow{z}% %The z is a dummy.
+       \def\lasttwo@nextrow{z}% %The z is a dummy.
+       \def\lastnine{z0000}% %The 0000 stands for the white space to the left. The z is a dummy.
+       \def\ignore@finderB@at{0}%
+       \qr@for \j=1 to \qr@size by 1%
+         {\edef\newbit{\qr@matrixentry{#1}{\the\i}{\the\j}}%
+          %
+          % LASTFIVE CODE FOR PENALTY 1
+          % First, add the new bit to the end.
+          \xa\g@addto@macro\xa\lastfive\xa{\newbit}%
+          \ifnum\j<5\relax%
+            %Not yet on the 5th entry.
+            %Don't do any testing.
+          \else
+            % 5th entry or later.
+            % Remove the old one, and then test.
+            \removefirsttoken\lastfive%
+            \ifx\lastfive\@fiveones%
+              \ifstringoffive%
+                %This is a continuation of a previous block of five or more 1's.
+                \stepcounter{penaltyi}%
+              \else
+                %This is a new string of five 1's.
+                \addtocounter{penaltyi}{\qr@Ni}%
+                \global\stringoffivetrue
+              \fi
+            \else
+              \ifx\lastfive\@fivezeros%
+                \ifstringoffive
+                  %This is a continuation of a previous block of five or more 0's.
+                  \stepcounter{penaltyi}%
+                \else
+                  %This is a new string of five 0's.
+                  \addtocounter{penaltyi}{\qr@Ni}%
+                  \global\stringoffivetrue
+                \fi
+              \else
+                %This is not a string of five 1's or five 0's.
+                \global\stringoffivefalse
+              \fi
+            \fi
+          \fi
+          %
+          % 2x2 BLOCKS FOR PENALTY 2
+          % Every 2x2 block of all 1's counts for \qr@Nii penalty points.
+          % We do not need to run this test in the last row.
+          \xa\ifnum\xa\i\xa<\qr@size\relax
+            \xa\g@addto@macro\xa\lasttwo@thisrow\xa{\newbit}%
+            %Compute \iplusone
+            \qr@a=\i\relax%
+            \advance\qr@a by 1%
+            \edef\iplusone{\the\qr@a}%
+            %
+            \edef\nextrowbit{\qr@matrixentry{#1}{\iplusone}{\the\j}}%
+            \xa\g@addto@macro\xa\lasttwo@nextrow\xa{\nextrowbit}%
+            \ifnum\j<2\relax%
+              %Still in the first column; no check.
+            \else
+              %Second column or later.  Remove the old bits, and then test.
+              \removefirsttoken\lasttwo@thisrow
+              \removefirsttoken\lasttwo@nextrow
+              \ifx\lasttwo@thisrow\@twoones
+                \ifx\lasttwo@nextrow\@twoones
+                  \addtocounter{penaltyii}{\qr@Nii}%
+                \fi
+              \else
+                \ifx\lasttwo@thisrow\@twozeros
+                  \ifx\lasttwo@nextrow\@twozeros
+                    \addtocounter{penaltyii}{\qr@Nii}%
+                  \fi
+                \fi
+              \fi
+            \fi
+          \fi
+          %
+          % LASTNINE CODE FOR PENALTY 3
+          % First, add the new bit to the end.
+          \xa\g@addto@macro\xa\lastnine\xa{\newbit}%
+          \ifnum\j<7\relax%
+            %Not yet on the 7th entry.
+            %Don't do any testing.
+          \else
+            % 7th entry or later.
+            % Remove the old one, and then test.
+            \removefirsttoken\lastnine
+            \xa\ifnum\qr@size=\j\relax%
+              % Last column.  Any of the following should count:
+              %     1011101 (\@finderB@zero)
+              %    10111010 (\@finderB@one)
+              %   101110100 (\@finderB@two)
+              %  1011101000 (\@finderB@three)
+              % 10111010000 (\@finderB)
+              \ifx\lastnine\@finderB
+                \addpenaltyiii
+              \else
+                \removefirsttoken\lastnine
+                \ifx\lastnine\@finderB@three
+                  \addpenaltyiii
+                \else
+                  \removefirsttoken\lastnine
+                  \ifx\lastnine\@finderB@two
+                    \addpenaltyiii
+                  \else
+                    \removefirsttoken\lastnine
+                    \ifx\lastnine\@finderB@one
+                      \addpenaltyiii
+                    \else
+                      \removefirsttoken\lastnine
+                      \ifx\lastnine\@finderB@zero
+                        \addpenaltyiii
+                      \fi
+                    \fi
+                  \fi
+                \fi
+              \fi
+            \else
+              \ifx\lastnine\@finderA% %Matches 0000 1011101
+                \addpenaltyiii
+                %Also, we record our discovery, so that we can't count this pattern again
+                %if it shows up four columns later as 1011101 0000.
+                %
+                %Set \ignore@finderB@at to \j+4.
+                \qr@a=\j\relax%
+                \advance\qr@a by 4%
+                \xdef\ignore@finderB@at{\the\qr@a}%
+              \else
+                \ifx\lastfive\@finderB% %Matches 1011101 0000.
+                  \xa\ifnum\ignore@finderB@at=\j\relax
+                    %This pattern was *not* counted already earlier.
+                    \addpenaltyiii
+                  \fi
+                \fi
+              \fi
+            \fi
+          \fi
+          %
+          %COUNT 1's FOR PENALTY 4
+          \xa\ifnum\newbit=1\relax%
+            \stepcounter{totalones}%
+          \fi
+         }% end of j-loop
+      }% end of i-loop
+    %
+    %NOW WE ALSO NEED TO RUN DOWN THE COLUMNS TO FINISH CALCULATING PENALTIES 1 AND 3.
+    \qr@for \j=1 to \qr@size by 1%
+      {\def\lastfive{z}% %The z is a dummy, that will be removed before any testing.
+       \stringoffivefalse
+       \def\lastnine{z0000}% %The 0000 stands for the white space to the left. The z is a dummy.
+       \def\ignore@finderB@at{0}%
+       \qr@for \i=1 to \qr@size by 1%
+         {\edef\newbit{\qr@matrixentry{#1}{\the\i}{\the\j}}%
+          %
+          % LASTFIVE CODE FOR PENALTY 1
+          % First, add the new bit to the end.
+          \xa\g@addto@macro\xa\lastfive\xa{\newbit}%
+          \ifnum\i<5\relax%
+            %Not yet on the 5th entry.
+            %Don't do any testing.
+          \else
+            % 5th entry or later.
+            % Remove the old one, and then test.
+            \removefirsttoken\lastfive%
+            \ifx\lastfive\@fiveones%
+              \ifstringoffive%
+                %This is a continuation of a previous block of five or more 1's.
+                \stepcounter{penaltyi}%
+              \else
+                %This is a new string of five 1's.
+                \addtocounter{penaltyi}{\qr@Ni}%
+                \global\stringoffivetrue
+              \fi
+            \else
+              \ifx\lastfive\@fivezeros%
+                \ifstringoffive
+                  %This is a continuation of a previous block of five or more 0's.
+                  \stepcounter{penaltyi}%
+                \else
+                  %This is a new string of five 0's.
+                  \addtocounter{penaltyi}{\qr@Ni}%
+                  \global\stringoffivetrue
+                \fi
+              \else
+                %This is not a string of five 1's or five 0's.
+                \global\stringoffivefalse
+              \fi
+            \fi
+          \fi
+          %
+          % HAPPILY, WE DON'T NEED TO CALCULATE PENALTY 2 AGAIN.
+          %
+          % LASTNINE CODE FOR PENALTY 3
+          % First, add the new bit to the end.
+          \xa\g@addto@macro\xa\lastnine\xa{\newbit}%
+          \ifnum\i<7\relax%
+            %Not yet on the 7th entry.
+            %Don't do any testing.
+          \else
+            % 7th entry or later.
+            % Remove the old one, and then test.
+            \removefirsttoken\lastnine
+            \xa\ifnum\qr@size=\i\relax%
+              % Last column.  Any of the following should count:
+              %     1011101 (\@finderB@zero)
+              %    10111010 (\@finderB@one)
+              %   101110100 (\@finderB@two)
+              %  1011101000 (\@finderB@three)
+              % 10111010000 (\@finderB)
+              \ifx\lastnine\@finderB
+                \addpenaltyiii
+              \else
+                \removefirsttoken\lastnine
+                \ifx\lastnine\@finderB@three
+                  \addpenaltyiii
+                \else
+                  \removefirsttoken\lastnine
+                  \ifx\lastnine\@finderB@two
+                    \addpenaltyiii
+                  \else
+                    \removefirsttoken\lastnine
+                    \ifx\lastnine\@finderB@one
+                      \addpenaltyiii
+                    \else
+                      \removefirsttoken\lastnine
+                      \ifx\lastnine\@finderB@zero
+                        \addpenaltyiii
+                      \fi
+                    \fi
+                  \fi
+                \fi
+              \fi
+            \else
+              \ifx\lastnine\@finderA% %Matches 0000 1011101
+                \addpenaltyiii
+                %Also, we record our discovery, so that we can't count this pattern again
+                %if it shows up four columns later as 1011101 0000.
+                %
+                %Set \ignore@finderB@at to \i+4.
+                \qr@a=\i\relax%
+                \advance\qr@a by 4%
+                \xdef\ignore@finderB@at{\the\qr@a}%
+              \else
+                \ifx\lastfive\@finderB% %Matches 1011101 0000.
+                  \xa\ifnum\ignore@finderB@at=\i\relax
+                    %This pattern was *not* counted already earlier.
+                    \addpenaltyiii
+                  \fi
+                \fi
+              \fi
+            \fi
+          \fi
+          %
+         }% end of i-loop
+      }% end of j-loop
+  \egroup%
+  %
+  %CALCULATE PENALTY 4
+  %According to the spec, penalty #4 is computed as
+  % floor( |(i/n^2)-0.5|/0.05 )
+  % where i is the total number of 1's in the matrix.
+  % This is equal to abs(20*i-10n^2) div n^2.
+  %
+  \qr@a=\totalones\relax
+  \multiply\qr@a by 20\relax
+  \qr@b=\qr@size\relax
+  \multiply\qr@b by \qr@size\relax
+  \qr@c=10\relax
+  \multiply\qr@c by \qr@b\relax
+  \advance\qr@a by -\qr@c\relax
+  \ifnum\qr@a<0\relax
+    \multiply\qr@a by -1\relax
+  \fi
+  \divide\qr@a by \qr@b\relax
+  \setcounter{penaltyiv}{\the\qr@a}%
+  %
+  %CALCULATE TOTAL PENALTY
+  \qr@a=\the\penaltyi\relax%
+  \advance\qr@a by \the\penaltyii\relax%
+  \advance\qr@a by \the\penaltyiii\relax%
+  \advance\qr@a by \the\penaltyiv\relax%
+  \edef\qr@penalty{\the\qr@a}%
+}%
+
+\def\removefirsttoken#1{%
+  %Removes the first token from the macro named in #1.
+  \edef\qr@argument{(#1)}%
+  \xa\removefirsttoken@int\qr@argument%
+  \xdef#1{\removefirsttoken@result}%
+}%
+\def\removefirsttoken@int(#1#2){%
+  \def\removefirsttoken@result{#2}%
+}%
+
+\def\qr@writeformatstring#1#2{%
+  % #1 = matrix name
+  % #2 = binary string representing the encoded and masked format information
+  \setcounter{qr@i}{9}%
+  \setcounter{qr@j}{1}%
+  \edef\qr@argument{{#1}(#2\relax)}%
+  \xa\qr@writeformatA@recursive\qr@argument
+  %
+  \setcounter{qr@i}{\qr@numberofrowsinmatrix{#1}}%
+  \setcounter{qr@j}{9}%
+  \xa\qr@writeformatB@recursive\qr@argument
+}%
+
+\def\qr@writeformatA@recursive#1(#2#3){%
+  % #1 = matrix name
+  % #2 = first bit of string
+  % #3 = rest of bitstream
+  % (qr@i,qr@j) = current (valid) position to write (in LaTeX counters)
+  \def\formattowrite{#3}%
+  \ifnum#2=1\relax
+    \qr@storetomatrix{#1}{\theqr@i}{\theqr@j}{\qr@black@format}%
+  \else
+    \qr@storetomatrix{#1}{\theqr@i}{\theqr@j}{\qr@white@format}%
+  \fi
+  % Now the tricky part--moving \i and \j to their next positions.
+  \ifnum\qr@j<9\relax
+    %If we're not yet in column 9, move right.
+    \stepcounter{qr@j}%
+    \ifnum\qr@j=7\relax
+      %But we skip column 7!
+      \stepcounter{qr@j}%
+    \fi
+  \else
+    %If we're in column 9, we move up.
+    \addtocounter{qr@i}{-1}%
+    \ifnum\qr@i=7\relax
+      %But we skip row 7!
+      \addtocounter{qr@i}{-1}%
+    \fi
+  \fi
+  %N.B. that at the end of time, this will leave us at invalid position (0,9).
+  %That makes for an easy test to know when we are done.
+  \ifnum\qr@i<1
+    \let\nexttoken=\relax
+  \else
+    \def\nexttoken{\qr@writeformatA@recursive{#1}(#3)}%
+  \fi
+  \nexttoken
+}%
+
+\def\qr@writeformatB@recursive#1(#2#3){%
+  % #1 = matrix name
+  % #2 = first bit of string
+  % #3 = rest of bitstream
+  % (qr@i,qr@j) = current (valid) position to write (in LaTeX counters)
+  \def\formattowrite{#3}%
+  \ifnum#2=1\relax
+    \qr@storetomatrix{#1}{\theqr@i}{\theqr@j}{\qr@black@format}%
+  \else
+    \qr@storetomatrix{#1}{\theqr@i}{\theqr@j}{\qr@white@format}%
+  \fi
+  % Now the tricky part--moving counters i and j to their next positions.
+  \qr@a=\qr@size%
+  \advance\qr@a by -6\relax%
+  \ifnum\qr@a<\qr@i\relax
+    %If we're not yet in row n-6, move up.
+    \addtocounter{qr@i}{-1}%
+  \else
+    \ifnum\qr@a=\qr@i\relax
+      %If we're actually in row n-6, we jump to position (9,n-7).
+      \setcounter{qr@i}{9}%
+      %Set counter j equal to \qr@size-7.
+      \global\qr@j=\qr@size\relax%
+      \global\advance\qr@j by -7\relax%
+    \else
+      %Otherwise, we must be in row 9.
+      %In this case, we move right.
+      \stepcounter{qr@j}%
+    \fi
+  \fi
+  %N.B. that at the end of time, this will leave us at invalid position (9,n+1).
+  %That makes for an easy test to know when we are done.
+  \xa\ifnum\qr@size<\qr@j\relax
+    \let\nexttoken=\relax
+  \else
+    \def\nexttoken{\qr@writeformatB@recursive{#1}(#3)}%
+  \fi
+  \nexttoken
+}%
+
+\def\qr@writeversionstring#1#2{%
+  % #1 = matrix name
+  % #2 = binary string representing the encoded version information
+  %
+  % Plot the encoded version string into the matrix.
+  % This is only done for versions 7 and higher.
+  \xa\ifnum\qr@version>6\relax
+    %Move to position (n-8,6).
+    \setcounter{qr@i}{\qr@size}\relax%
+    \addtocounter{qr@i}{-8}\relax%
+    \setcounter{qr@j}{6}%
+    \edef\qr@argument{{#1}(#2\relax)}%
+    \xa\qr@writeversion@recursive\qr@argument
+  \fi
+}%
+
+\def\qr@writeversion@recursive#1(#2#3){%
+  % #1 = matrix name
+  % #2 = first bit of string
+  % #3 = rest of bitstream
+  % (qr@i,qr@j) = current (valid) position to write (in LaTeX counters)
+  %
+  % The version information is stored symmetrically in the matrix
+  % In two transposed regions, so we can write both at the same time.
+  % In the comments, we describe what happens in the lower-left region,
+  % not the upper-right.
+  %
+  \def\versiontowrite{#3}%
+  %
+  %Set \topline equal to n-10.
+  \qr@a=\qr@size\relax%
+  \advance\qr@a by -10\relax%
+  \edef\topline{\the\qr@a}%
+  %
+  \ifnum#2=1\relax
+    \qr@storetomatrix{#1}{\theqr@i}{\theqr@j}{\qr@black@format}%
+    \qr@storetomatrix{#1}{\theqr@j}{\theqr@i}{\qr@black@format}%
+  \else
+    \qr@storetomatrix{#1}{\theqr@i}{\theqr@j}{\qr@white@format}%
+    \qr@storetomatrix{#1}{\theqr@j}{\theqr@i}{\qr@white@format}%
+  \fi
+  % Now the tricky part--moving counters i and j to their next positions.
+  \addtocounter{qr@i}{-1}%
+  \xa\ifnum\topline>\qr@i\relax
+    %We've overshot the top of the region.
+    %We need to move left one column and down three.
+    \addtocounter{qr@j}{-1}%
+    \addtocounter{qr@i}{3}%
+  \fi
+  %N.B. that at the end of time, this will leave us at invalid position (n-8,0).
+  %That makes for an easy test to know when we are done.
+  \ifnum\qr@j<1\relax
+    \let\nexttoken=\relax
+  \else
+    \def\nexttoken{\qr@writeversion@recursive{#1}(#3)}%
+  \fi
+  \nexttoken
+}%
+\newcount\qr@hexchars
+
+\def\qr@string@binarytohex#1{%
+  \qr@binarytohex{\qr@hex@result}{#1}%
+}%
+
+\def\qr@encode@binary#1{%
+  % #1 = string of ascii characters, to be converted into bitstream
+  %
+  % We do this one entirely in hex, rather than binary, because we can.
+  \edef\plaintext{#1}%
+  %
+  %First, the mode indicator.
+  \def\qr@codetext{4}% %This means `binary'
+  %
+  %Next, the character count.
+  \qr@getstringlength{\plaintext}%
+  %Set \charactercountlengthinhex to \qr@charactercountbits@byte/4%
+  \qr@a=\qr@charactercountbits@byte\relax%
+  \divide \qr@a by 4\relax%
+  \edef\charactercountlengthinhex{\the\qr@a}%
+  \qr@decimaltohex[\charactercountlengthinhex]{\charactercount}{\qr@stringlength}%
+  \xa\g@addto@macro\xa\qr@codetext\xa{\charactercount}%
+  %
+  %Now comes the actual data.
+  \edef\qr@argument{(,\plaintext\relax\relax\relax)}%
+  \xa\qr@encode@ascii@recursive\qr@argument%
+  %
+  %Now the terminator.
+  \g@addto@macro\qr@codetext{0}% %This is '0000' in binary.
+  %
+  %There is no need to pad bits to make a multiple of 8,
+  %because the data length is already 4 + 8 + 8n + 4.
+  %
+  %Now add padding codewords if needed.
+  \setcounter{qr@hexchars}{0}%
+  \qr@getstringlength{\qr@codetext}%
+  \setcounter{qr@hexchars}{\qr@stringlength}%
+  %Set \qr@numpaddingcodewords equal to \qr@totaldatacodewords - hexchars/2.
+  \qr@a=-\qr@hexchars\relax
+  \divide\qr@a by 2\relax
+  \advance\qr@a by \qr@totaldatacodewords\relax
+  \edef\qr@numpaddingcodewords{\the\qr@a}%
+  %
+  \xa\ifnum\qr@numpaddingcodewords<0%
+    \edef\ds{ERROR: Too much data!  Over by \qr@numpaddingcodewords bytes.}\show\ds%
+  \fi%
+  \xa\ifnum\qr@numpaddingcodewords>0%
+    \qr@for \i = 2 to \qr@numpaddingcodewords by 2%
+      {\g@addto@macro{\qr@codetext}{ec11}}%
+    \xa\ifodd\qr@numpaddingcodewords\relax%
+      \g@addto@macro{\qr@codetext}{ec}%
+    \fi%
+  \fi%
+}%
+
+\def\qr@encode@ascii@recursive(#1,#2#3){%
+  % #1 = hex codes translated so far
+  % #2 = next plaintext character to translate
+  % #3 = remainder of plaintext
+  \edef\testii{#2}%
+  \ifx\testii\@relax%
+    % All done!
+    \g@addto@macro\qr@codetext{#1}%
+  \else%
+    % Another character to translate.
+    \edef\asciicode{\number`#2}%
+    \qr@decimaltohex[2]{\newhexcodes}{\asciicode}%
+    \edef\qr@argument{(#1\newhexcodes,#3)}%
+    %\show\qr@argument
+    \xa\qr@encode@ascii@recursive\qr@argument%
+  \fi%
+}%
+
+\def\qr@splitcodetextintoblocks{%
+  \setcounter{qr@i}{0}%
+  \qr@for \j = 1 to \qr@numshortblocks by 1%
+    {\stepcounter{qr@i}%
+     \qr@splitoffblock{\qr@codetext}{\theqr@i}{\qr@shortblock@size}%
+    }%
+  \xa\ifnum\qr@numlongblocks>0\relax%
+    \qr@for \j = 1 to \qr@numlongblocks by 1%
+      {\stepcounter{qr@i}%
+       \qr@splitoffblock{\qr@codetext}{\theqr@i}{\qr@longblock@size}%
+      }%
+  \fi%
+}%
+
+\def\qr@splitoffblock#1#2#3{%
+  % #1 = current codetext in hexadecimal
+  % #2 = number to use in csname "\datablock@#2".
+  % #3 = number of bytes to split off
+  \qrmessage{<Splitting off block #2>}%
+  \xa\gdef\csname datablock@#2\endcsname{}% %This line is important!
+  \qr@for \i = 1 to #3 by 1%
+    {\edef\qr@argument{{#2}(#1)}%
+     \xa\qr@splitoffblock@int\qr@argument%
+    }%
+}%
+
+\def\qr@splitoffblock@int#1(#2#3#4){%
+  % #1 = number to use in csname "\datablock@#1".
+  % #2#3 = next byte to split off
+  % #4 = remaining text
+  %
+  % We add the next byte to "\datablock@#1",
+  % and we remove it from the codetext.
+  \xa\xdef\csname datablock@#1\endcsname{\csname datablock@#1\endcsname#2#3}%
+  \xdef\qr@codetext{#4}%
+}%
+
+\def\qr@createerrorblocks{%
+  \qr@for \ii = 1 to \qr@numblocks by 1%
+    {\qrmessage{<Making error block \the\ii>}%
+     \FX@generate@errorbytes{\csname datablock@\the\ii\endcsname}{\qr@num@eccodewords}%
+     \xa\xdef\csname errorblock@\the\ii\endcsname{\FX@errorbytes}%
+    }%
+}%
+
+\def\qr@interleave{%
+  \setcounter{qr@i}{0}%
+  \def\qr@interleaved@text{}%
+  \qrmessage{<Interleaving datablocks of length \qr@shortblock@size\space and \qr@longblock@size: }%
+  \qr@for \ii = 1 to \qr@shortblock@size by 1%
+    {\qr@for \jj = 1 to \qr@numblocks by 1%
+      {\qr@writefromblock{datablock}{\the\jj}%
+      }%
+     \qrmessage{\the\ii,}%
+    }%
+  %The long blocks are numbered \qr@numshortblocks+1, \qr@numshortblocks+2, ..., \qr@numblocks.
+  \qr@a=\qr@numshortblocks\relax%
+  \advance\qr@a by 1\relax%
+  \qr@for \jj = \qr@a to \qr@numblocks by 1%
+      {\qr@writefromblock{datablock}{\the\jj}}%
+  \xa\ifnum\qr@numlongblocks>0\relax%
+    \qrmessage{\qr@longblock@size.>}%
+  \else
+    \qrmessage{.>}%
+  \fi
+  \qrmessage{<Interleaving errorblocks of length \qr@num@eccodewords: }%
+  \qr@for \ii = 1 to \qr@num@eccodewords by 1%
+    {\qrmessage{\the\ii,}%
+     \qr@for \jj = 1 to \qr@numblocks by 1%
+      {\qr@writefromblock{errorblock}{\the\jj}%
+      }%
+    }%
+  \qrmessage{.><Interleaving complete.>}%
+}%
+
+\def\qr@writefromblock#1#2{%
+  % #1 = either 'datablock' or 'errorblock'
+  % #2 = block number, in {1,...,\qr@numblocks}%
+  \edef\qr@argument{(\csname #1@#2\endcsname\relax\relax\relax)}%
+  \xa\qr@writefromblock@int\qr@argument
+  \xa\xdef\csname #1@#2\endcsname{\qr@writefromblock@remainder}%
+}%
+
+\def\qr@writefromblock@int(#1#2#3){%
+  % #1#2 = first byte (in hex) of text, which will be written to \qr@interleaved@text
+  % #3 = remainder, including \relax\relax\relax terminator.
+  \g@addto@macro{\qr@interleaved@text}{#1#2}%
+  \qr@writefromblock@intint(#3)%
+}%
+
+\def\qr@writefromblock@intint(#1\relax\relax\relax){%
+  \xdef\qr@writefromblock@remainder{#1}%
+}%
+\let\xa=\expandafter
+
+\def\preface@macro#1#2{%
+  % #1 = macro name
+  % #2 = text to add to front of macro
+  \def\tempb{#2}%
+  \xa\xa\xa\gdef\xa\xa\xa#1\xa\xa\xa{\xa\tempb #1}%
+}%
+
+\newif\ifqr@leadingcoeff
+\def\qr@testleadingcoeff(#1#2){%
+  % Tests whether the leading digit of #1#2 is 1.
+  \ifnum#1=1\relax
+    \qr@leadingcoefftrue
+  \else
+    \qr@leadingcoefffalse
+  \fi
+}%
+
+\def\qr@polynomialdivide#1#2{%
+  \edef\qr@numerator{#1}%
+  \edef\qr@denominator{#2}%
+  \qr@divisiondonefalse%
+  \xa\xa\xa\qr@oneroundofdivision\xa\xa\xa{\xa\qr@numerator\xa}\xa{\qr@denominator}%
+}%
+
+\def\@qr@empty{}%
+\def\qr@oneroundofdivision#1#2{%
+  % #1 = f(x), of degree n
+  % #2 = g(x), of degree m
+  % Obtains a new polynomial h(x), congruent to f(x) modulo g(x),
+  % but of degree at most n-1.
+  %
+  % If leading coefficient of f(x) is 1, subtracts off g(x) * x^(n-m).
+  % If leading coefficient of f(x) is 0, strips off that leading zero.
+  %
+  \qr@testleadingcoeff(#1)%
+  \ifqr@leadingcoeff
+    \qr@xorbitstrings{#1}{#2}%
+    \ifqr@xorfailed
+      %If xor failed, that means our #1 was already the remainder!
+      \qr@divisiondonetrue
+      \edef\theremainder{#1}%
+    \else
+      %xor succeeded. We need to recurse.
+      \xa\xa\xa\edef\xa\xa\xa\qr@numerator\xa\xa\xa{\xa\qr@stripleadingzero\xa(\xorresult)}%
+    \fi
+  \else
+    \xa\def\xa\qr@numerator\xa{\qr@stripleadingzero(#1)}%
+    \ifx\qr@numerator\@qr@empty
+      \qr@divisiondonetrue
+      \def\theremainder{0}%
+    \fi
+  \fi
+  \ifqr@divisiondone
+    \relax
+  \else
+    \xa\qr@oneroundofdivision\xa{\qr@numerator}{#2}%
+  \fi
+}%
+
+\def\qr@stripleadingzero(0#1){#1}%Strips off a leading zero.
+
+\newif\ifqr@xorfailed% This flag will trigger when #2 is longer than #1.
+
+\def\qr@xorbitstrings#1#2{%
+ % #1 = bitstring
+ % #2 = bitstring no longer than #1
+ \qr@xorfailedfalse
+ \edef\qr@argument{(,#1\relax\relax)(#2\relax\relax)}%
+ \xa\qr@xorbitstrings@recursive\qr@argument
+ %\qr@xorbitstrings@recursive(,#1\relax\relax)(#2\relax\relax)%
+}%
+
+\def\qr@xorbitstrings@recursive(#1,#2#3)(#4#5){%
+ % #1#2#3 is the first bitstring, xor'ed up through #1.
+ % #4#5 is the remaining portion of the second bitstring.
+ \def\testii{#2}%
+ \def\testiv{#4}%
+ \ifx\testii\@relax
+   % #1 contains the whole string.
+   % Now if #4 is also \relax, that means the two strings started off with equal lengths.
+   % If, however, #4 is not \relax, that means the second string was longer than the first, a problem.
+   \ifx\testiv\@relax
+     %No problem.  We are done.
+     \qr@xorbit@saveresult(#1#2#3)%
+   \else
+     %Problem!  The second string was longer than the first.
+     \qr@xorfailedtrue
+     \def\xorresult{}%
+   \fi
+ \else
+   % There is still a bit to manipulate in #2.
+   % Check whether #4 contains anything.
+   \ifx\testiv\@relax
+     % No, #4 is empty.  We are done. "#2#3" contains the remainder of the first string,
+     % which we append untouched and then strip off the two \relax-es.
+     \qr@xorbit@saveresult(#1#2#3)%
+   \else
+     % Yes, #4 still has something to XOR. Do the task.
+     \ifnum#2=#4\relax
+       \qr@xorbitstrings@recursive(#1%
+                                 0,#3)(#5)%
+     \else
+       \qr@xorbitstrings@recursive(#1%
+                                 1,#3)(#5)%
+     \fi
+   \fi
+ \fi
+}%
+
+\def\qr@xorbit@saveresult(#1\relax\relax){%
+  %Strips off the extra '\relax'es at the end.
+  \def\xorresult{#1}%
+}%
+
+\newif\ifqr@divisiondone
+\def\dodivision#1#2{%
+  \qr@divisiondonefalse
+  \dodivision@recursive{#1}{#2}%
+}%
+
+\def\BCHcode#1{%
+  \edef\formatinfo{#1}%
+  \def\formatinfopadded{\formatinfo 0000000000}%
+  \def\qr@divisor{10100110111}%
+  \qr@divisiondonefalse
+  \qr@polynomialdivide{\formatinfopadded}{\qr@divisor}%
+  %
+  \qr@getstringlength{\theremainder}%
+  %Run loop from stringlength+1 to 10.
+  \qr@a=\qr@stringlength\relax%
+  \advance\qr@a by 1\relax%
+  \qr@for \i = \qr@a to 10 by 1%
+    {\preface@macro{\theremainder}{0}%
+     \xdef\theremainder{\theremainder}%
+    }%
+  \edef\BCHresult{\formatinfo\theremainder}%
+}%
+
+\def\qr@formatmask{101010000010010}%
+
+\def\qr@encodeandmaskformat#1{%
+  \BCHcode{#1}%
+  \qr@xorbitstrings{\BCHresult}{\qr@formatmask}%
+  \edef\qr@format@bitstring{\xorresult}%
+}%
+
+\def\qr@Golaycode#1{%
+  % #1 = 6-bit version number
+  \edef\qr@versioninfo{#1}%
+  \def\qr@versioninfopadded{\qr@versioninfo 000000000000}% %Append 12 zeros.
+  \def\qr@divisor{1111100100101}%
+  \qr@divisiondonefalse
+  \qr@polynomialdivide{\qr@versioninfopadded}{\qr@divisor}%
+  %
+  \qr@getstringlength{\theremainder}%
+  %Run loop from stringlength+1 to 12.
+  \qr@a=\qr@stringlength\relax%
+  \advance\qr@a by 1\relax%
+  \qr@for \i = \qr@a to 12 by 1%
+    {\preface@macro{\theremainder}{0}%
+     \xdef\theremainder{\theremainder}%
+    }%
+  \edef\Golayresult{\qr@versioninfo\theremainder}%
+}%
+\def\F@result{}%
+
+\def\qr@xorbitstring#1#2#3{%
+  % #1 = new macro to receive result
+  % #2, #3 = bitstrings to xor.  The second can be shorter than the first.
+  \def\qr@xor@result{}%
+  \edef\qr@argument{(#2\relax\relax)(#3\relax\relax)}%
+  \xa\qr@xorbitstring@recursive\qr@argument%
+  \edef#1{\qr@xor@result}%
+}%
+\def\qr@xorbitstring@recursive(#1#2)(#3#4){%
+  \edef\testi{#1}%
+  \ifx\testi\@relax%
+    %Done.
+    \let\qr@next=\relax%
+  \else
+    \if#1#3\relax
+      \g@addto@macro{\qr@xor@result}{0}%
+    \else
+      \g@addto@macro{\qr@xor@result}{1}%
+    \fi
+    \edef\qr@next{\noexpand\qr@xorbitstring@recursive(#2)(#4)}%
+  \fi
+  \qr@next
+}
+
+\def\F@addchar@raw#1#2{%
+  %Add two hexadecimal digits using bitwise xor
+  \qr@hextobinary[4]{\summandA}{#1}%
+  \qr@hextobinary[4]{\summandB}{#2}%
+  \qr@xorbitstring{\F@result}{\summandA}{\summandB}%
+  \qr@binarytohex[1]{\F@result}{\F@result}%
+}%
+
+\def\canceltwos#1{%
+  \edef\qr@argument{(#1\relax\relax)}%
+  \xa\canceltwos@int\qr@argument%
+}%
+
+\def\canceltwos@int(#1#2){%
+  \xa\canceltwos@recursion(,#1#2)%
+}%
+
+\def\canceltwos@recursion(#1,#2#3){%
+  \def\testii{#2}%
+  \ifx\testii\@relax
+    %Cancelling complete.
+    \striptworelaxes(#1#2#3)%
+    %Now \F@result contains the answer.
+  \else
+    \relax
+    \ifnum#2=2\relax
+      \canceltwos@recursion(#10,#3)%
+    \else
+      \canceltwos@recursion(#1#2,#3)%
+    \fi
+  \fi
+}%
+
+\def\striptworelaxes(#1\relax\relax){%
+  \gdef\F@result{#1}%
+}%
+
+\qr@for \i = 0 to 15 by 1%
+  {\qr@decimaltohex[1]{\qr@tempa}{\the\i}%
+   \qr@for \j = 0 to 15 by 1%
+    {\qr@decimaltohex[1]{\qr@tempb}{\the\j}%
+     \F@addchar@raw\qr@tempa\qr@tempb
+     \xa\xdef\csname F@addchar@\qr@tempa\qr@tempb\endcsname{\F@result}%
+    }%
+  }%
+
+\def\F@addchar#1#2{%
+  \xa\def\xa\F@result\xa{\csname F@addchar@#1#2\endcsname}%
+}%
+
+\def\F@addstrings#1#2{%
+  \edef\qr@argument{(,#1\relax\relax)(#2\relax\relax)}%
+  \xa\F@addstrings@recursion\qr@argument%
+}%
+
+\def\F@addstrings@recursion(#1,#2#3)(#4#5){%
+  %Adds two hexadecimal strings, bitwise, from left to right.
+  %The second string is allowed to be shorter than the first.
+  \def\testii{#2}%
+  \def\testiv{#4}%
+  \ifx\testii\@relax
+    %The entire string has been processed.
+    \gdef\F@result{#1}%
+  \else
+    \ifx\testiv\@relax
+      %The second string is over.
+      \striptworelaxes(#1#2#3)%
+      %Now \F@result contains the answer.
+    \else
+      %We continue to add.
+      \F@addchar{#2}{#4}%
+      \edef\qr@argument{(#1\F@result,#3)(#5)}%
+      \xa\F@addstrings@recursion\qr@argument%
+    \fi
+  \fi
+}%
+\gdef\F@stripleadingzero(0#1){\edef\F@result{#1}}%
+
+\qr@i=0%
+\def\poweroftwo{1}%
+\qr@for \i = 1 to 254 by 1%
+  {\global\advance\qr@i by1%
+   \qr@a=\poweroftwo\relax
+   \multiply\qr@a by 2\relax
+   \edef\poweroftwo{\the\qr@a}%
+   %\show\poweroftwo
+   \qr@decimaltohex[2]{\poweroftwo@hex}{\poweroftwo}%
+   \xa\ifnum\poweroftwo>255\relax
+     %We need to bitwise add the polynomial represented by 100011101, i.e. 0x11d.
+     \F@addstrings{\poweroftwo@hex}{11d}%               %Now it should start with 0.
+     \xa\F@stripleadingzero\xa(\F@result)%              %Now it should be two hex digits.
+     \edef\poweroftwo@hex{\F@result}%                   %Save the hex version.
+     \qr@hextodecimal{\poweroftwo}{\F@result}%
+   \fi
+   \xdef\poweroftwo{\poweroftwo}%
+   \xa\xdef\csname F@twotothe@\theqr@i\endcsname{\poweroftwo@hex}%
+   \xa\xdef\csname F@logtwo@\poweroftwo@hex\endcsname{\theqr@i}%
+  }%
+\xa\xdef\csname F@twotothe@0\endcsname{01}%
+\xa\xdef\csname F@logtwo@01\endcsname{0}%
+
+\def\F@twotothe#1{%
+  \xa\xdef\xa\F@result\xa{\csname F@twotothe@#1\endcsname}%
+}%
+\def\F@logtwo#1{%
+  \xa\xdef\xa\F@result\xa{\csname F@logtwo@#1\endcsname}%
+}%
+
+\def\@zerozero{00}%
+
+\def\F@multiply#1#2{%
+  % #1 and #2 are two elements of F_256,
+  % given as two-character hexadecimal strings.
+  % Multiply them within F_256, and place the answer in \F@result
+  \edef\argA{#1}%
+  \edef\argB{#2}%
+  \ifx\argA\@zerozero
+    \def\F@result{00}%
+  \else
+    \ifx\argB\@zerozero
+      \def\F@result{00}%
+    \else
+      \xa\F@logtwo\xa{\argA}%
+        \edef\logA{\F@result}%
+      \xa\F@logtwo\xa{\argB}%
+        \edef\logB{\F@result}%
+      \xa\qr@a\xa=\logA\relax%  \qr@a = \logA
+      \xa\advance\xa\qr@a\logB\relax% \advance \qr@a by \logB
+      \ifnum\qr@a>254\relax%
+        \advance\qr@a by -255\relax%
+      \fi%
+      \xa\F@twotothe\xa{\the\qr@a}%
+      % Now \F@result contains the product, as desired.
+    \fi
+  \fi
+}%
+
+\def\F@multiply#1#2{%
+  % #1 and #2 are two elements of F_256,
+  % given as two-character hexadecimal strings.
+  % Multiply them within F_256, and place the answer in \F@result
+  \edef\argA{#1}%
+  \edef\argB{#2}%
+  \ifx\argA\@zerozero
+    \def\F@result{00}%
+  \else
+    \ifx\argB\@zerozero
+      \def\F@result{00}%
+    \else
+      \xa\F@logtwo\xa{\argA}%
+        \edef\logA{\F@result}%
+      \xa\F@logtwo\xa{\argB}%
+        \edef\logB{\F@result}%
+      \xa\qr@a\xa=\logA\relax%  \qr@a = \logA
+      \xa\advance\xa\qr@a\logB\relax% \advance \qr@a by \logB
+      \ifnum\qr@a>254\relax%
+        \advance\qr@a by -255\relax%
+      \fi%
+      \xa\F@twotothe\xa{\the\qr@a}%
+      % Now \F@result contains the product, as desired.
+    \fi
+  \fi
+}%
+
+\def\FX@getstringlength#1{%
+  %Count number of two-character coefficients
+  \setcounter{qr@i}{0}%
+  \xdef\qr@argument{(#1\relax\relax\relax)}%
+  \xa\FX@stringlength@recursive\qr@argument%
+  \xdef\stringresult{\arabic{qr@i}}%
+}%
+
+\def\FX@stringlength@recursive(#1#2#3){%
+  \def\testi{#1}%
+  \ifx\testi\@relax
+    %we are done.
+  \else
+    \stepcounter{qr@i}%
+    %\showthe\c@qr@i
+    \qr@stringlength@recursive(#3)%
+  \fi
+}%
+
+\newif\ifFX@leadingcoeff@zero
+\def\FX@testleadingcoeff(#1#2#3){%
+  % Tests whether the leading coefficient of the hex-string #1#2#3 is '00'.
+  \edef\FX@leadingcoefficient{#1#2}%
+  \FX@leadingcoeff@zerofalse
+  \ifx\FX@leadingcoefficient\@zerozero
+    \FX@leadingcoeff@zerotrue
+  \fi
+}%
+
+\newif\ifFX@divisiondone
+
+\newcount\qr@divisionsremaining %Keep track of how many divisions to go!
+\def\FX@polynomialdivide#1#2{%
+  \edef\FX@numerator{#1}%
+  \edef\denominator{#2}%
+  \qr@getstringlength\FX@numerator%
+  \setcounter{qr@divisionsremaining}{\qr@stringlength}%
+  \qr@getstringlength\denominator%
+  \addtocounter{qr@divisionsremaining}{-\qr@stringlength}%
+  \addtocounter{qr@divisionsremaining}{2}%
+  \divide\qr@divisionsremaining by 2\relax% %2 hex chars per number
+  \FX@divisiondonefalse%
+  \xa\xa\xa\FX@polynomialdivide@recursive\xa\xa\xa{\xa\FX@numerator\xa}\xa{\denominator}%
+}%
+
+\def\FX@polynomialdivide@recursive#1#2{%
+  % #1 = f(x), of degree n
+  % #2 = g(x), of degree m
+  % Obtains a new polynomial h(x), congruent to f(x) modulo g(x),
+  % but of degree at most n-1.
+  %
+  % If leading coefficient of f(x) is 0, strips off that leading zero.
+  % If leading coefficient of f(x) is a, subtracts off a * g(x) * x^(n-m).
+  % N.B. we assume g is monic.
+  %
+  \FX@testleadingcoeff(#1)%
+  \ifFX@leadingcoeff@zero%
+    %Leading coefficient is zero, so remove it.
+    \xa\def\xa\FX@numerator\xa{\FX@stripleadingzero(#1)}%
+  \else%
+    %Leading coefficient is nonzero, and contained in \FX@leadingcoefficient
+    \FX@subtractphase{#1}{#2}{\FX@leadingcoefficient}%
+    \ifFX@subtract@failed%
+      %If subtraction failed, that means our #1 was already the remainder!
+      \FX@divisiondonetrue%
+      \edef\theremainder{#1}%
+    \else%
+      %xor succeeded. We need to recurse.
+      \xa\xa\xa\edef\xa\xa\xa\FX@numerator\xa\xa\xa{\xa\FX@stripleadingzero\xa(\FX@subtraction@result)}%
+    \fi%
+  \fi%
+  \addtocounter{qr@divisionsremaining}{-1}%
+  \ifnum\qr@divisionsremaining=0\relax
+    %Division is done!
+    \FX@divisiondonetrue%
+    \edef\theremainder{\FX@numerator}%
+    \relax%
+  \else%
+    \xa\FX@polynomialdivide@recursive\xa{\FX@numerator}{#2}%
+  \fi%
+}%
+
+\def\FX@stripleadingzero(00#1){#1}%Strips off a single leading zero of F_256.
+
+\newif\ifFX@subtract@failed% This flag will trigger when #2 is longer than #1.
+
+\def\FX@subtractphase#1#2#3{%
+ % #1 = bitstring
+ % #2 = bitstring no longer than #1
+ % #3 = leading coefficient
+ \FX@subtract@failedfalse%
+ \edef\qr@argument{(,#1\relax\relax\relax)(#2\relax\relax\relax)(#3)}%
+ \xa\FX@subtract@recursive\qr@argument%
+}%
+
+\def\FX@subtract@recursive(#1,#2#3#4)(#5#6#7)(#8){%
+ % This is a recursive way to compute f(x) - a*g(x)*x^k.
+ % #1#2#3#4 is the first bitstring, subtracted up through #1.
+ %          Thus #2#3 constitutes the next two-character coefficient.
+ % #5#6#7 is the remaining portion of the second bitstring.
+ %          Thus #5#6 constitutes the next two-character coefficient
+ % #8 is the element a of F_256.  It should contain two characters.
+ \def\testii{#2}%
+ \def\testv{#5}%
+ \ifx\testii\@relax
+   % #1 contains the whole string.
+   % Now if #5 is also \relax, that means the two strings started off with equal lengths.
+   % If, however, #5 is not \relax, that means the second string was longer than the first, a problem.
+   \ifx\testv\@relax
+     %No problem.  We are done.
+     \FX@subtract@saveresult(#1#2#3#4)% %We keep the #2#3#4 to be sure we have all three relax-es to strip off.
+   \else
+     %Problem!  The second string was longer than the first.
+     %This usually indicates the end of the long division process.
+     \FX@subtract@failedtrue
+     \def\FX@subtraction@result{}%
+   \fi
+ \else
+   % There is still a coefficient to manipulate in #2#3.
+   % Check whether #5 contains anything.
+   \ifx\testv\@relax
+     % No, #5 is empty.  We are done. "#2#3#4" contains the remainder of the first string,
+     % which we append untouched and then strip off the three \relax-es.
+     \FX@subtract@saveresult(#1#2#3#4)%
+   \else
+     % Yes, #5#6 still has something to XOR. Do the task.
+     \F@multiply{#5#6}{#8}% Multiply by the factor 'a'.
+     \F@addstrings{#2#3}{\F@result}% Subtract.  (We're in characteristic two, so adding works.)
+     \edef\qr@argument{(#1\F@result,#4)(#7)(#8)}%
+     \xa\FX@subtract@recursive\qr@argument%
+   \fi
+ \fi
+}%
+
+\def\FX@subtract@saveresult(#1\relax\relax\relax){%
+  %Strips off the three extra '\relax'es at the end.
+  \def\FX@subtraction@result{#1}%
+}%
+
+\def\FX@creategeneratorpolynomial#1{%
+  % #1 = n, the number of error codewords desired.
+  % We need to create \prod_{j=0}^{n-1} (x-2^j).
+  \edef\FX@generator@degree{#1}%
+  \def\FX@generatorpolynomial{01}% Initially, set it equal to 1.
+  \setcounter{qr@i}{0}%
+  \FX@creategenerator@recursive%
+  %The result is now stored in \FX@generatorpolynomial
+}%
+
+\def\FX@creategenerator@recursive{%
+  % \c@qr@i contains the current value of i.
+  % \FX@generatorpolynomial contains the current polynomial f(x),
+  %   which should be a degree-i polynomial
+  %   equal to \prod_{j=0}^{i-1} (x-2^j).
+  %   (If i=0, then \FX@generatorpolynomial should be 01.)
+  % This recursion step should multiply the existing polynomial by (x-2^i),
+  % increment i by 1, and check whether we're done or not.
+  \edef\summandA{\FX@generatorpolynomial 00}% This is f(x) * x
+  \edef\summandB{00\FX@generatorpolynomial}% This is f(x), with a 0x^{i+1} in front.
+  \F@twotothe{\theqr@i}%
+  \edef\theconstant{\F@result}%
+  \FX@subtractphase{\summandA}{\summandB}{\theconstant}%
+     %This calculates \summandA + \theconstant * \summandB
+     %and stores the result in \FX@subtraction@result
+  \edef\FX@generatorpolynomial{\FX@subtraction@result}%
+  \stepcounter{qr@i}%
+  \xa\ifnum\FX@generator@degree=\qr@i\relax%
+    %We just multiplied by (x-2^{n-1}), so we're done.
+    \relax%
+  \else%
+    %We need to do this again!
+    \xa%
+    \FX@creategenerator@recursive%
+  \fi%
+}%
+
+\def\FX@generate@errorbytes#1#2{%
+  % #1 = datastream in hex
+  % #2 = number of error correction bytes requested
+  \edef\numerrorbytes{#2}%
+  \xa\FX@creategeneratorpolynomial\xa{\numerrorbytes}%
+  \edef\FX@numerator{#1}%
+  \qr@for \i = 1 to \numerrorbytes by 1%
+    {\g@addto@macro\FX@numerator{00}}% %One error byte means two hex codes.
+  \FX@polynomialdivide{\FX@numerator}{\FX@generatorpolynomial}%
+  \edef\FX@errorbytes{\theremainder}%
+}%
+\newif\ifqr@versionmodules
+
+\def\qr@level@char#1{%
+    \xa\ifcase#1
+      M\or L\or H\or Q\fi}%
+
+\newif\ifqr@versiongoodenough
+\def\qr@choose@best@version#1{%
+  % \qr@desiredversion = user-requested version
+  % \qr@desiredlevel = user-requested error-correction level
+  \edef\qr@plaintext{#1}%
+  \qr@getstringlength{\qr@plaintext}%
+  %
+  %Run double loop over levels and versions, looking for
+  %the smallest version that can contain our data,
+  %and then choosing the best error-correcting level at that version,
+  %subject to the level being at least as good as the user desires.
+  \global\qr@versiongoodenoughfalse%
+  \gdef\qr@bestversion{0}%
+  \gdef\qr@bestlevel{0}%
+  \ifnum\qr@desiredversion=0\relax
+    \qr@a=1\relax
+  \else
+    \qr@a=\qr@desiredversion\relax
+  \fi
+  \qr@for \i=\qr@a to 40 by 1
+    {\edef\qr@version{\the\i}%
+     \global\qr@versiongoodenoughfalse
+     \qr@for \j=0 to 3 by 1%
+      {%First, we map {0,1,2,3} to {1,0,4,3}, so that we loop through {M,L,H,Q}
+       %in order of increasing error-correction capabilities.
+       \qr@a = \j\relax
+       \divide \qr@a by 2\relax
+       \multiply \qr@a by 4\relax
+       \advance \qr@a by 1\relax
+       \advance \qr@a by -\j\relax
+       \edef\qr@level{\the\qr@a}%
+       \ifnum\qr@desiredlevel=\qr@a\relax
+         \global\qr@versiongoodenoughtrue
+       \fi
+       \ifqr@versiongoodenough
+         \qr@calculate@capacity{\qr@version}{\qr@level}%
+         \xa\xa\xa\ifnum\xa\qr@truecapacity\xa<\qr@stringlength\relax
+           %Too short
+           \relax
+         \else
+           %Long enough!
+           \xdef\qr@bestversion{\qr@version}%
+           \xdef\qr@bestlevel{\qr@level}%
+           \global\i=40%
+         \fi
+       \fi
+      }%
+     }%
+  \edef\qr@version{\qr@bestversion}%
+  \edef\qr@level{\qr@bestlevel}%
+  \xa\ifnum\qr@desiredversion>0\relax
+    \ifx\qr@bestversion\qr@desiredversion\relax
+      %No change from desired version.
+    \else
+      %Version was increased
+      \qrmessage{<Requested QR version '\qr@desiredversion' is too small for desired text.}%
+      \qrmessage{Version increased to '\qr@bestversion' to fit text.>^^J}%
+    \fi
+  \fi
+  \ifx\qr@bestlevel\qr@desiredlevel\relax
+    %No change in level.
+  \else
+    \qrmessage{<Error-correction level increased from \qr@level@char{\qr@desiredlevel}}%
+    \qrmessage{to \qr@level@char{\qr@bestlevel} at no cost.>^^J}%
+  \fi
+}%
+
+\def\qr@calculate@capacity#1#2{%
+  \edef\qr@version{#1}%
+  \edef\qr@level{#2}%
+  %Calculate \qr@size, the number of modules per side.
+  % The formula is 4\qr@version+17.
+  \qr@a=\qr@version\relax%
+  \multiply\qr@a by 4\relax%
+  \advance\qr@a by 17\relax%
+  \xdef\qr@size{\the\qr@a}%
+  %
+  % Calculate \qr@k, which governs the number of alignment patterns.
+  % The alignment patterns lie in a kxk square, except for 3 that are replaced by finding patterns.
+  % The formula is 2 + floor( \qr@version / 7 ), except that k=0 for version 1.
+  \xa\ifnum\qr@version=1\relax%
+    \def\qr@k{0}%
+  \else%
+    \qr@a=\qr@version\relax
+    \divide \qr@a by 7\relax
+    \advance\qr@a by 2\relax
+    \edef\qr@k{\the\qr@a}%
+  \fi%
+  %
+  %Calculate number of function pattern modules.
+  %This consists of the three 8x8 finder patterns, the two timing strips, and the (k^2-3) 5x5 alignment patterns.
+  %The formula is 160+2n+25(k^2-3)-10(k-2), unless k=0 in which case we just have 160+2n.
+  \qr@a=\qr@size\relax
+  \multiply\qr@a by 2\relax
+  \advance\qr@a by 160\relax
+  \xa\ifnum\qr@k=0\relax\else
+    %\qr@k is nonzero, hence at least 2, so we continue to add 25(k^2-3)-10(k-2).
+    \qr@b=\qr@k\relax
+    \multiply\qr@b by \qr@k\relax
+    \advance\qr@b by -3\relax
+    \multiply\qr@b by 25\relax
+    \advance\qr@a by \qr@b\relax
+    \qr@b=\qr@k\relax
+    \advance\qr@b by -2\relax
+    \multiply\qr@b by 10\relax
+    \advance\qr@a by -\qr@b\relax
+  \fi
+  \edef\qr@numfunctionpatternmodules{\the\qr@a}%
+  %
+  %Calculate the number of version modules, either 36 or 0.
+  \xa\ifnum\qr@version>6\relax
+    \qr@versionmodulestrue
+    \def\qr@numversionmodules{36}%
+  \else
+    \qr@versionmodulesfalse
+    \def\qr@numversionmodules{0}%
+  \fi
+  %
+  %Now calculate the codeword capacity and remainder bits.
+  %Take n^2 modules, subtract all those dedicated to finder patterns etc., format information, and version information,
+  %and what's left is the number of bits we can play with.
+  %The number of complete bytes is \qr@numdatacodewords;
+  %the leftover bits are \qr@numremainderbits.
+  \qr@a=\qr@size\relax
+  \multiply \qr@a by \qr@size\relax
+  \advance \qr@a by -\qr@numfunctionpatternmodules\relax
+  \advance \qr@a by -31\relax% % There are 31 format modules.
+  \advance \qr@a by -\qr@numversionmodules\relax
+  \qr@b=\qr@a\relax
+  \divide \qr@a by 8\relax
+  \edef\qr@numdatacodewords{\the\qr@a}%
+  \multiply\qr@a by 8\relax
+  \advance \qr@b by -\qr@a\relax
+  \edef\qr@numremainderbits{\the\qr@b}%
+  %
+  %The size of the character count indicator also varies by version.
+  %There are only two options, so hardcoding seems easier than expressing these functionally.
+  \xa\ifnum\qr@version<10\relax
+    \def\qr@charactercountbytes@byte{1}%
+    \def\qr@charactercountbits@byte{8}%
+  \else
+    \def\qr@charactercountbytes@byte{2}%
+    \def\qr@charactercountbits@byte{16}%
+  \fi
+  %
+  %Now we call on the table, from the QR specification,
+  %of how many blocks to divide the message into, and how many error bytes each block gets.
+  %This affects the true capacity for data, which we store into \qr@totaldatacodewords.
+  % The following macro sets \qr@numblocks and \qr@num@eccodewords
+  % based on Table 9 of the QR specification.
+  \qr@settableix
+  \qr@a = -\qr@numblocks\relax
+  \multiply \qr@a by \qr@num@eccodewords\relax
+  \advance\qr@a by \qr@numdatacodewords\relax
+  \edef\qr@totaldatacodewords{\the\qr@a}%
+  \advance\qr@a by -\qr@charactercountbytes@byte\relax%Subtract character count
+  \advance\qr@a by -1\relax% Subtract 1 byte for the 4-bit mode indicator and the 4-bit terminator at the end.
+  \edef\qr@truecapacity{\the\qr@a}%
+}
+
+\def\qr@setversion#1#2{%
+  % #1 = version number, an integer between 1 and 40 inclusive.
+  % #2 = error-correction level, as an integer between 0 and 3 inclusive.
+  %      0 = 00 = M
+  %      1 = 01 = L
+  %      2 = 10 = H
+  %      3 = 11 = Q
+  % This macro calculates and sets a variety of global macros and/or counters
+  % storing version information that is used later in construction the QR code.
+  % Thus \setversion should be called every time!
+  %
+  \edef\qr@version{#1}%
+  \edef\qr@level{#2}%
+  %
+  \qr@calculate@capacity{\qr@version}{\qr@level}%
+  %The capacity-check code sets the following:
+  % * \qr@size
+  % * \qr@k
+  % * \ifqr@versionmodules
+  % * \qr@numversionmodules
+  % * \qr@numdatacodewords
+  % * \qr@numremainderbits
+  % * \qr@charactercountbits@byte
+  % * \qr@charactercountbytes@byte
+  % * \qr@numblocks (via \qr@settableix)
+  % * \qr@num@eccodewords (via \qr@settableix)
+  % * \qr@totaldatacodewords
+  %
+  % The alignment patterns' square is 7 modules in from each edge.
+  % They are spaced "as evenly as possible" with an even number of modules between each row/column,
+  % unevenness in division being accommodated by making the first such gap smaller.
+  % The formula seems to be
+  %    general distance = 2*round((n-13)/(k-1)/2+0.25)
+  %                     = 2*floor((n-13)/(k-1)/2+0.75)
+  %                     = 2*floor( (2*(n-13)/(k-1)+3) / 4 )
+  %                     = (((2*(n-13)) div (k-1) + 3 ) div 4 ) * 2
+  %    first distance = leftovers
+  % The 0.25 is to accommodate version 32, which is the only time we round down.
+  % Otherwise a simple 2*ceiling((n-13)/(k-1)/2) would have sufficed.
+  %
+  \qr@a = \qr@size\relax
+  \advance\qr@a by -13\relax
+  \multiply\qr@a by 2\relax
+  \qr@b = \qr@k\relax
+  \advance \qr@b by -1\relax
+  \divide\qr@a by \qr@b\relax
+  \advance\qr@a by 3\relax
+  \divide\qr@a by 4\relax
+  \multiply\qr@a by 2\relax
+  \edef\qr@alignment@generalskip{\the\qr@a}%
+  %
+  %Now set \qr@alignment@firstskip to (\qr@size-13)-(\qr@k-2)*\qr@alignment@generalskip %
+  \qr@a = \qr@k\relax
+  \advance\qr@a by -2\relax
+  \multiply\qr@a by -\qr@alignment@generalskip\relax
+  \advance\qr@a by \qr@size\relax
+  \advance\qr@a by -13\relax
+  \edef\qr@alignment@firstskip{\the\qr@a}%
+  %
+  %
+  %
+  % Our \qr@totaldatacodewords bytes of data are broken up as evenly as possible
+  % into \qr@numblocks datablocks; some may be one byte longer than others.
+  % We set \qr@shortblock@size to floor(\qr@totaldatacodewords / \qr@numblocks)
+  % and \qr@numlongblocks to mod(\qr@totaldatacodewords , \qr@numblocks).
+  \qr@a=\qr@totaldatacodewords\relax
+  \divide\qr@a by \qr@numblocks\relax
+  \edef\qr@shortblock@size{\the\qr@a}%
+  \multiply\qr@a by -\qr@numblocks\relax
+  \advance\qr@a by \qr@totaldatacodewords\relax
+  \edef\qr@numlongblocks{\the\qr@a}%
+  %
+  %Set \qr@longblock@size to \qr@shortblock@size+1.
+  \qr@a=\qr@shortblock@size\relax
+  \advance\qr@a by 1\relax
+  \edef\qr@longblock@size{\the\qr@a}%
+  %
+  %Set \qr@numshortblocks to \qr@numblocks - \qr@numlongblocks
+  \qr@b=\qr@numblocks\relax
+  \advance\qr@b by -\qr@numlongblocks\relax
+  \edef\qr@numshortblocks{\the\qr@b}%
+}%
+
+\def\qr@settableix@int(#1,#2){%
+  \edef\qr@numblocks{#1}%
+  \edef\qr@num@eccodewords{#2}%
+}%
+
+\def\qr@settableix{%
+\xa\ifcase\qr@level\relax
+  %00: Level 'M', medium error correction
+  \edef\tempdata{(%
+    \ifcase\qr@version\relax
+      \relax %There is no version 0.
+    \or1,10%
+    \or1,16%
+    \or1,26%
+    \or2,18%
+    \or2,24%
+    \or4,16%
+    \or4,18%
+    \or4,22%
+    \or5,22%
+    \or5,26%
+    \or5,30%
+    \or8,22%
+    \or9,22%
+    \or9,24%
+    \or10,24%
+    \or10,28%
+    \or11,28%
+    \or13,26%
+    \or14,26%
+    \or16,26%
+    \or17,26%
+    \or17,28%
+    \or18,28%
+    \or20,28%
+    \or21,28%
+    \or23,28%
+    \or25,28%
+    \or26,28%
+    \or28,28%
+    \or29,28%
+    \or31,28%
+    \or33,28%
+    \or35,28%
+    \or37,28%
+    \or38,28%
+    \or40,28%
+    \or43,28%
+    \or45,28%
+    \or47,28%
+    \or49,28%
+  \fi)}%
+\or
+  %01: Level 'L', low error correction
+  \edef\tempdata{%
+  (\ifcase\qr@version\relax
+    \relax %There is no version 0.
+  \or 1,7%
+  \or 1,10%
+  \or 1,15%
+  \or 1,20%
+  \or 1,26%
+  \or 2,18%
+  \or 2,20%
+  \or 2,24%
+  \or 2,30%
+  \or 4,18%
+  \or 4,20%
+  \or 4,24%
+  \or 4,26%
+  \or 4,30%
+  \or 6,22%
+  \or 6,24%
+  \or 6,28%
+  \or 6,30%
+  \or 7,28%
+  \or 8,28%
+  \or 8,28%
+  \or 9,28%
+  \or 9,30%
+  \or 10,30%
+  \or 12,26%
+  \or 12,28%
+  \or 12,30%
+  \or 13,30%
+  \or 14,30%
+  \or 15,30%
+  \or 16,30%
+  \or 17,30%
+  \or 18,30%
+  \or 19,30%
+  \or 19,30%
+  \or 20,30%
+  \or 21,30%
+  \or 22,30%
+  \or 24,30%
+  \or 25,30%
+  \fi)}%
+\or
+  %10: Level 'H', high error correction
+  \edef\tempdata{(%
+    \ifcase\qr@version\relax
+      \relax %There is no version 0.
+    \or1,17%
+    \or1,28%
+    \or2,22%
+    \or4,16%
+    \or4,22%
+    \or4,28%
+    \or5,26%
+    \or6,26%
+    \or8,24%
+    \or8,28%
+    \or11,24%
+    \or11,28%
+    \or16,22%
+    \or16,24%
+    \or18,24%
+    \or16,30%
+    \or19,28%
+    \or21,28%
+    \or25,26%
+    \or25,28%
+    \or25,30%
+    \or34,24%
+    \or30,30%
+    \or32,30%
+    \or35,30%
+    \or37,30%
+    \or40,30%
+    \or42,30%
+    \or45,30%
+    \or48,30%
+    \or51,30%
+    \or54,30%
+    \or57,30%
+    \or60,30%
+    \or63,30%
+    \or66,30%
+    \or70,30%
+    \or74,30%
+    \or77,30%
+    \or81,30%
+  \fi)}%
+\or
+  %11: Level 'Q', quality error correction
+  \edef\tempdata{(%
+    \ifcase\qr@version\relax
+      \relax %There is no version 0.
+    \or1,13%
+    \or1,22%
+    \or2,18%
+    \or2,26%
+    \or4,18%
+    \or4,24%
+    \or6,18%
+    \or6,22%
+    \or8,20%
+    \or8,24%
+    \or8,28%
+    \or10,26%
+    \or12,24%
+    \or16,20%
+    \or12,30%
+    \or17,24%
+    \or16,28%
+    \or18,28%
+    \or21,26%
+    \or20,30%
+    \or23,28%
+    \or23,30%
+    \or25,30%
+    \or27,30%
+    \or29,30%
+    \or34,28%
+    \or34,30%
+    \or35,30%
+    \or38,30%
+    \or40,30%
+    \or43,30%
+    \or45,30%
+    \or48,30%
+    \or51,30%
+    \or53,30%
+    \or56,30%
+    \or59,30%
+    \or62,30%
+    \or65,30%
+    \or68,30%
+    \fi)}%
+\fi
+\xa\qr@settableix@int\tempdata
+}%
+
+\def\@qr@M{M}\def\@qr@z{0}%
+\def\@qr@L{L}\def\@qr@i{1}%
+\def\@qr@H{H}\def\@qr@ii{2}%
+\def\@qr@Q{Q}\def\@qr@iii{3}%
+\def\qr@setlevel#1{%
+  \edef\qr@level@selected{#1}%
+  \ifx\qr@level@selected\@qr@M
+    \edef\qr@desiredlevel{0}%
+  \fi
+  \ifx\qr@level@selected\@qr@L
+    \edef\qr@desiredlevel{1}%
+  \fi
+  \ifx\qr@level@selected\@qr@H
+    \edef\qr@desiredlevel{2}%
+  \fi
+  \ifx\qr@level@selected\@qr@Q
+    \edef\qr@desiredlevel{3}%
+  \fi
+  \ifx\qr@level@selected\@qr@z
+    \edef\qr@desiredlevel{0}%
+  \fi
+  \ifx\qr@level@selected\@qr@i
+    \edef\qr@desiredlevel{1}%
+  \fi
+  \ifx\qr@level@selected\@qr@ii
+    \edef\qr@desiredlevel{2}%
+  \fi
+  \ifx\qr@level@selected\@qr@iii
+    \edef\qr@desiredlevel{3}%
+  \fi
+}%
+
+% key-value pairs (OPmac trick 0069)
+\def\kv#1{\expandafter\ifx\csname kv:#1\endcsname \relax \expandafter\kvunknown
+   \else \csname kv:#1\expandafter\endcsname\fi 
+}
+\def\kvunknown{???}
+\def\kvscan #1#2=#3,{\ifx#1,\else \sdef{kv:#1#2}{#3}\expandafter\kvscan\fi}
+
+\ifx\replacestrings\undefined
+\bgroup \catcode`!=3 \catcode`?=3
+\gdef\replacestrings#1#2{\long\def\replacestringsA##1#1##2!{%
+   \ifx!##2!\addto\tmpb{##1}\else\addto\tmpb{##1#2}\replacestringsA##2!\fi}%
+   \edef\tmpb{\expandafter}\expandafter\replacestringsA\tmpb?#1!%
+   \long\def\replacestringsA##1?{\def\tmpb{##1}}\expandafter\replacestringsA\tmpb
+}
+\egroup
+\long\def\addto#1#2{\expandafter\def\expandafter#1\expandafter{#1#2}}
+\def\sdef#1{\expandafter\def\csname#1\endcsname}
+\fi
+
+\def\qrset#1{\def\tmpb{#1,}%
+   \replacestrings{ =}{=}\replacestrings{= }{=}%
+   \replacestrings{tight,}{qr-border=0,}%
+   \replacestrings{padding,}{qr-border=1,}%
+   \replacestrings{verbose,}{qr-message=1,}%
+   \replacestrings{silent,}{qr-message=0,}%
+   \replacestrings{draft,}{qr-final=0,}%
+   \replacestrings{final,}{qr-final=1,}%
+   \replacestrings{nolink,}{qr-link=0,}%
+   \replacestrings{link,}{qr-link=1,}%
+   \expandafter\kvscan\tmpb,=,%
+   \qrdesiredheight=\kv{height}\relax
+   \qr@setlevel{\kv{level}}%
+   \edef\qr@desiredversion{\kv{version}}%
+}
+\qrset{height=2cm, version=0, level=M, tight, verbose, final, nolink}
+
+\def\qrcode{\begingroup
+   % LaTeX ballast:
+   \def\setcounter##1##2{\global\csname##1\endcsname=##2\relax}%
+   \def\stepcounter##1{\global\advance\csname##1\endcsname by1\relax}%
+   \def\addtocounter##1##2{\global\advance\csname##1\endcsname by##2\relax}%
+   \let\xa=\expandafter \newlinechar=`\^^J
+   \isnextchar[{\qrcodeA}{\qrcodeB}%
+}
+\def\qrcodeA[#1]{\qrset{#1}\expandafter\qrcodeB\romannumeral-`\.}
+\def\qrcodeB{%
+   \ifx\mubyteout\undefined \else \mubyteout=0 \mubytelog=0 \fi
+   \def\xprncodesave{}%
+   \ifx\xprncodes\undefined \else 
+      \ifnum\xprncode255=0 \def\xprncodesave{\xprncodes=0 }\xprncodes=1 \fi\fi
+   \if1\kv{qr-message}\let\qrmessage=\message \else \def\qrmessage##1{}\fi
+   \if1\kv{qr-border}\def\padd{\kern4\qrmodulesize}\else\def\padd{}\fi
+   \bgroup \qrverbatim \qrcode@i
+}
+\def\qrcode@i#1{\xdef\qretext{#1}\gdef\qrtext{#1}\egroup
+   \qrcode@int
+   \xprncodesave
+   \endgroup
+}
+
+\def\qrcode@int{%
+  \qrmessage{<QR code requested for "\qretext" in version
+           \qr@desiredversion-\qr@level@char{\qr@desiredlevel}.>^^J}%
+  %First, choose the version and level.
+  %Recall that \qr@choose@best@version sets \qr@version and \qr@level.
+  \xa\qr@choose@best@version\xa{\qretext}%
+  \if1\kv{qr-final}%
+     \qr@setversion{\qr@version}{\qr@level}%
+     \qrcode@int@new
+  \else
+     \qrmodulesize=\qrdesiredheight%
+     \divide\qrmodulesize by \qr@size\relax%
+     \let\d=\qrdesiredheight
+     \vbox{\padd\hbox{\padd\vbox to\d{\hrule\vss
+        \hbox to\d{\vrule height.7\d depth.3\d \hss ...QR...\hss\vrule}%
+        \vss\hrule}\padd}\padd}%
+  \fi
+}%
+
+\def\qrcode@int@new{%
+  \qrbeginhook
+  \qr@createsquareblankmatrix{newqr}{\qr@size}%
+  \qr@placefinderpatterns{newqr}%
+  \qr@placetimingpatterns{newqr}%
+  \qr@placealignmentpatterns{newqr}%
+  \qr@placedummyformatpatterns{newqr}%
+  \qr@placedummyversionpatterns{newqr}%
+  \qrmessage{<Calculating QR code for "\qretext" in
+            version \qr@version-\qr@level@char{\qr@level}.>^^J}%
+  \xa\qr@encode@binary\xa{\qretext}%
+  \qr@splitcodetextintoblocks
+  \qr@createerrorblocks
+  \qr@interleave
+  \qrmessage{<Writing data...}%
+     \qr@writedata@hex{newqr}{\qr@interleaved@text}%
+  \qrmessage{done.>^^J}%
+  \qr@writeremainderbits{newqr}%
+  \qr@chooseandapplybestmask{newqr}%
+  \qr@decimaltobinary[2]{\level@binary}{\qr@level}%
+  \qr@decimaltobinary[3]{\mask@binary}{\qr@mask@selected}%
+  \edef\formatstring{\level@binary\mask@binary}%
+  \qrmessage{<Encoding and writing format string...}%
+     \xa\qr@encodeandmaskformat\xa{\formatstring}%
+     \qr@writeformatstring{newqr}{\qr@format@bitstring}%
+  \qrmessage{done.>^^J}%
+  \qrmessage{<Encoding and writing version information...}%
+     \qr@decimaltobinary[6]{\version@binary}{\qr@version}%
+     \qr@Golaycode{\version@binary}%
+     \qr@writeversionstring{newqr}{\Golayresult}%
+  \qrmessage{done.>^^J}%
+  \qrmessage{<Printing QR code...}%
+     \qrmatrixtobinary{newqr}%
+     \qrrestore\qrdata
+  \qrmessage{done.>^^J}%
+  \qrendhook
+}%
+
+\def\qrmatrixtobinary#1{%
+   \bgroup
+   \gdef\qrdata{}%
+   \def\qr@black{1}\let\qr@black@fixed=\qr@black \let\qr@black@format=\qr@black
+   \def\@white{0}\let\qr@white@fixed=\@white \let\qr@white@format=\@white
+   \qr@for \i = 1 to \qr@size by 1
+      {\qr@for \j = 1 to \qr@size by 1
+          {\xdef\qrdata{\qrdata\qr@matrixentry{#1}{\the\i}{\the\j}}}}%
+   \xdef\qrdata{{\qr@size}{\qrdata}}%
+   \egroup
+}
+
+\def\qrrestore#1{\expandafter\qrrestoreA#1}
+\def\qrrestoreA#1#2{%
+   \qrmodulesize=\qrdesiredheight \divide\qrmodulesize by#1
+   \if1\kv{qr-link}\setbox0=\fi
+   \vbox\bgroup\padd \offinterlineskip \baselineskip=\qrmodulesize 
+      \qr@i=0 \qr@j=0 \let\next=\qrrestoreB 
+   \hbox\bgroup\padd \qrrestoreB #2%
+   \if1\kv{qr-link}\qr@link{\qretext}{\box0}\fi
+}
+\def\qrrestoreB#1{\advance \qr@j by1
+   \ifx1#1\vrule height\qrmodulesize width\qrmodulesize\else \kern\qrmodulesize\fi
+   \ifnum\qr@size=\qr@j \vrule height\qrmodulesize width 0pt \padd\egroup \advance\qr@i by1
+      \ifnum\qr@size=\qr@i \padd\egroup \let\next=\relax \else \hbox\bgroup\padd \fi
+   \fi \next
+}
+
+\def\qrbeginhook{}
+\def\qrendhook{}
+
+\tmp % \catcode of @ is returned back.
+
+\endinput
+
+
+Options
+-------
+
+You can use \qrset{options} for global-like options and
+\qrcode[options]{encoded text} for local options for one QR code.
+The \qrset{options} is valid within a group (if exists) or in whole
+document. 
+
+Options are separated by comma and they are in two types: single
+word or key=value format. Default options are: 
+
+\qrset{height=2cm, version=0, level=M, tight, verbose, final, nolink}
+
+The options are the same as described in qrcode.pdf at 
+http://www.ctan.org/tex-archive/macros/latex/contrib/qrcode.
+In short:
+
+height=dimen   ... The height of the QRcode without padding.
+
+version=number ... Number 0 to 40 linearly depends on the density of QRcode.
+                   The 0 means that the density is automatically selected.
+
+level=letter   ... L, M, Q o H (low, medium, quality, hight) sets the amount 
+                   of redundancy in the code in order of error recovering.
+
+tight   ... Code without margins.
+padding ... 4module blank margins around the code.
+
+verbose ... Information about calculating in terminal and in the log.
+silent  ... No information about calculating.
+
+final   ... The QR code is calculated and printed.
+draft   ... Only empty rectangle in the same size as QR code is printed.
+
+nolink  ... The QR code is not active hyperlink.
+link    ... The QR code is active hyperlink to "encoded text".
+            Note that link option works in pdfTeX (luaTeX) only.
+
+qrborder={R G B}  ... The color of the frame around active hypertext space
+                      if link option is set. R G B (red green blue) are decimal 
+                      numbers from 0 to 1. The frame is visible only in
+                      pdf viewers. Default: invisible frame.
+
+Example:
+
+\qrset{silent}     % ... all codes will be silent in the log and terminal.
+\qrcode [height=3cm, link, padding, qrborder={1 0 0}] {http://petr.olsak.net}  
+                   % ... 3cm QRcode as hyperlink
+
+Note:
+
+The saving/restoring pre-calculated QRcodes isn't supported by default.
+If you are printing the same QR codes repeatedly, use \setbox/\copy
+technique. For example:
+
+\newbox\mybox
+\setbox\mybox=\hbox{\qrcode{encoded text}}
+\copy\mybox \copy\mybox \copy\mybox etc.
+
+If you have a huge amount of different QR codes, you can use draft/final options
+or you can use REF file from OPmac. See the OPmac trick 
+
+   http://petr.olsak.net/opmac-tricks-e.html#qrcode
+
+The \qrdata macro is saved after each \qrcode calculation in the format
+{size}{111101011...001} where size is the number of columns or rows in QR
+square and second parameter includes size^2 ones or zeros which means black
+or white modules (scanned left to right, top to bottom). Another information
+can be retrieved from \qrtext macro (encoded text before expanding) and
+\qretext macro (encoded text where \{, \\ etc. are expanded to {, \ etc.).
+The macros \qrdata, \qrtext and \qretext are saved globally.
+
+
+Non-ASCII characters
+--------------------
+
+If you are using csplain with pdfTeX (no XeTeX, no LuaTeX) then UTF-8 input
+is correctly interpreted from \qrcode parameter. 
+
+The technical background: the encTeX's \mubyte is set to zero during
+scanning the \qrcode parameter, so the parameter is rawly UTF-8 encoded and
+this is correct for QR codes.
+
+Problems: 
+1. You cannot use \qrcode{parameter} inside another macro, bacause UTF-8
+   encoded parameter is reencoded already.
+2. You cannot use XeTeX or LuaTeX because UTF-8 encoded parameter is
+   reencoded to Unicode already. And the backward conversion from Unicode 
+   to UTF-8 isn't implemented here at macro level.
+
+
+History
+-------
+
+Jun. 2015  released
+Jul. 2015 \xprncodes=0space (bug fixed)
+Sep. 2018 \isnextchar processed in group
+May  2019 strut included for case of empty line (bug fixed)
diff --git a/mo/tex/test.tex b/mo/tex/test.tex
new file mode 100644
index 0000000000000000000000000000000000000000..84ad7c77f56117871613abc20ea87602d0e3e37e
--- /dev/null
+++ b/mo/tex/test.tex
@@ -0,0 +1,14 @@
+\input protokol.tex
+
+\def\kolo{Krajské kolo 70. ročníku Matematické olympiády}
+\def\kat{Kategorie P, Zlínský kraj}
+
+\proto{MO:70-P-III-1:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-1}
+\proto{MO:70-P-III-2:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-2}
+\proto{MO:70-P-III-3:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-3}
+\proto{MO:70-P-III-4:12345}{Pokusný Králík}{4/4}{Gymnázium Na Paloučku, Králíky}{P-III-4}
+\proto{MO:70-P-III-4:12345}{Pokusný Králík}{4/4}{MŠ, ZŠ a SŠ pro sluchově postižené, Valašské Meziříčí}{P-III-4}
+\universal
+\blank
+
+\bye
diff --git a/mo/tex/ucw-luaofs.tex b/mo/tex/ucw-luaofs.tex
new file mode 100644
index 0000000000000000000000000000000000000000..5008bea7902a68ebe51f816385e9f918ae9b22aa
--- /dev/null
+++ b/mo/tex/ucw-luaofs.tex
@@ -0,0 +1,216 @@
+\ucwdefmodule{luaofs}
+\ucwdefmodule{ofs}
+
+\input luaotfload.sty
+\input ofs.tex
+\nofontmessages
+
+\ofsputfamlist{^^JLatinModern:}
+
+\def\LMfeat#1{:mode=base;script=latn;+tlig}
+
+%%% LMRoman %%%
+
+\ofsdeclarefamily [LMRoman] {%
+   \loadtextfam lmr;%
+                lmbx;%
+                lmti;%
+                lmbxti;;%
+   \newvariant 8 \sl   (Slanted)     lmsl;;%
+   \newvariant 9 \bxsl (BoldSlanted) lmbxsl;;%
+   \newvariant a \bo   (BoldNormal)  lmb;;%
+   \newvariant b \bosl (BoldNormalSlanted)  lmbsl;;%
+   \newvariant c \csc  (CapsAndSmallCaps)   lmcsc;;%
+}
+
+\registertfm lmr        -      LMRoman10-Regular\LMfeat{}
+\registertfm lmr     0pt-6pt   LMRoman5-Regular\LMfeat{}
+\registertfm lmr     6pt-7pt   LMRoman6-Regular\LMfeat{}
+\registertfm lmr     7pt-8pt   LMRoman7-Regular\LMfeat{}
+\registertfm lmr     8pt-9pt   LMRoman8-Regular\LMfeat{}
+\registertfm lmr     9pt-10pt  LMRoman9-Regular\LMfeat{}
+\registertfm lmr    10pt-12pt  LMRoman10-Regular\LMfeat{}
+\registertfm lmr    12pt-17pt  LMRoman12-Regular\LMfeat{}
+\registertfm lmr    17pt-*     LMRoman17-Regular\LMfeat{}
+
+\registertfm lmbx       -      LMRoman10-Bold\LMfeat{}
+\registertfm lmbx    0pt-6pt   LMRoman5-Bold\LMfeat{}
+\registertfm lmbx    6pt-7pt   LMRoman6-Bold\LMfeat{}
+\registertfm lmbx    7pt-8pt   LMRoman7-Bold\LMfeat{}
+\registertfm lmbx    8pt-9pt   LMRoman8-Bold\LMfeat{}
+\registertfm lmbx    9pt-10pt  LMRoman9-Bold\LMfeat{}
+\registertfm lmbx   10pt-12pt  LMRoman10-Bold\LMfeat{}
+\registertfm lmbx   12pt-*     LMRoman12-Bold\LMfeat{}
+
+\registertfm lmti       -      LMRoman10-Italic\LMfeat{}
+\registertfm lmti    0pt-8pt   LMRoman7-Italic\LMfeat{}
+\registertfm lmti    8pt-9pt   LMRoman8-Italic\LMfeat{}
+\registertfm lmti    9pt-10pt  LMRoman9-Italic\LMfeat{}
+\registertfm lmti   10pt-12pt  LMRoman10-Italic\LMfeat{}
+\registertfm lmti   12pt-*     LMRoman12-Italic\LMfeat{}
+
+\registertfm lmbxti     -      LMRoman10-BoldItalic\LMfeat{}
+
+\registertfm lmsl       -      LMRomanSlant10-Regular\LMfeat{}
+\registertfm lmsl    0pt-9pt   LMRomanSlant8-Regular\LMfeat{}
+\registertfm lmsl    9pt-10pt  LMRomanSlant9-Regular\LMfeat{}
+\registertfm lmsl   10pt-12pt  LMRomanSlant10-Regular\LMfeat{}
+\registertfm lmsl   12pt-17pt  LMRomanSlant12-Regular\LMfeat{}
+\registertfm lmsl   17pt-*     LMRomanSlant17-Regular\LMfeat{}
+
+\registertfm lmbxsl     -      LMRomanSlant10-Bold\LMfeat{}
+
+\registertfm lmb        -      LMRomanDemi10-Regular\LMfeat{}
+
+\registertfm lmbsl      -      LMRomanDemi10-Oblique\LMfeat{}
+
+\registertfm lmcsc      -      LMRomanCaps10-Regular\LMfeat{}
+
+\setfonts[LMRoman/]
+
+%%% LMSans %%%
+
+\ofsdeclarefamily [LMSans] {%
+   \loadtextfam lmss;%
+                lmssbx;%
+                lmsso;%
+                lmssbo;;%
+}
+
+\registertfm lmss       -      LMSans10-Regular\LMfeat{}
+\registertfm lmss    0pt-9pt   LMSans8-Regular\LMfeat{}
+\registertfm lmss    9pt-10pt  LMSans9-Regular\LMfeat{}
+\registertfm lmss   10pt-12pt  LMSans10-Regular\LMfeat{}
+\registertfm lmss   12pt-17pt  LMSans12-Regular\LMfeat{}
+\registertfm lmss   17pt-*     LMSans17-Regular\LMfeat{}
+
+\registertfm lmssbx     -      LMSans10-Bold\LMfeat{}
+
+\registertfm lmsso      -      LMSans10-Oblique\LMfeat{}
+\registertfm lmsso   0pt-9pt   LMSans8-Oblique\LMfeat{}
+\registertfm lmsso   9pt-10pt  LMSans9-Oblique\LMfeat{}
+\registertfm lmsso  10pt-12pt  LMSans10-Oblique\LMfeat{}
+\registertfm lmsso  12pt-17pt  LMSans12-Oblique\LMfeat{}
+\registertfm lmsso  17pt-*     LMSans17-Oblique\LMfeat{}
+
+\registertfm lmssbo     -      LMSans10-BoldOblique\LMfeat{}
+
+%%% LMSansDC %%%
+
+\ofsdeclarefamily [LMSansDC] {%
+   \loadtextfam lmssdc;%
+                ;%
+                lmssdo;%
+                ;;%
+}
+
+\registertfm lmssdc     -      LMSansDemiCond10-Regular\LMfeat{}
+
+\registertfm lmssdo     -      LMSansDemiCond10-Oblique\LMfeat{}
+
+%%% LMMono %%%
+
+\ofsdeclarefamily [LMMono] {%
+   \loadtextfam lmtt;%
+                lmtk;%
+                lmtti;%
+                ;;%
+   \newvariant 8 \sl   (Slanted)     lmtto;;%
+   \newvariant 9 \bxsl (BoldSlanted) lmtko;;%
+   \newvariant c \csc  (CapsAndSmallCaps)   lmtcsc;;%
+}
+
+\registertfm lmtt       -      LMMono10-Regular\LMfeat{}
+\registertfm lmtt    0pt-9pt   LMMono8-Regular\LMfeat{}
+\registertfm lmtt    9pt-10pt  LMMono9-Regular\LMfeat{}
+\registertfm lmtt   10pt-12pt  LMMono10-Regular\LMfeat{}
+\registertfm lmtt   12pt-*     LMMono12-Regular\LMfeat{}
+
+\registertfm lmtk       -      LMMonoLt10-Bold\LMfeat{}
+
+\registertfm lmtti      -      LMMono10-Italic\LMfeat{}
+
+\registertfm lmtto      -      LMMonoSlant10-Regular\LMfeat{}
+
+\registertfm lmtko      -      LMMonoLt10-BoldOblique\LMfeat{}
+
+\registertfm lmtcsc     -      LMMonoCaps10-Regular\LMfeat{}
+
+\newfam\ttfam
+\loadmathfam\ttfam[/LMMono10-Regular\LMfeat{}]
+
+%%% LMMonoCondensed %%%
+
+\ofsdeclarefamily [LMMonoCondensed] {%
+   \loadtextfam lmtlc;%
+                ;%
+                lmtlco;%
+                ;;%
+}
+
+\registertfm lmtlc      -      LMMonoLtCond10-Regular\LMfeat{}
+
+\registertfm lmtlco     -      LMMonoLtCond10-Oblique\LMfeat{}
+
+%%% UCW extensions %%%
+
+\ofsputfamlist{^^JUCW:}
+
+\ofsdeclarefamily [BlackboardBold] {%
+   \loadtextfam bbm;%
+		;%
+		;%
+		;\defaultextraenc;%
+}
+
+\registertfm bbm     -      bbm10
+\registertfm bbm  0pt-6pt   bbm5
+\registertfm bbm  6pt-7pt   bbm6
+\registertfm bbm  7pt-8pt   bbm7
+\registertfm bbm  8pt-9pt   bbm8
+\registertfm bbm  9pt-10pt  bbm9
+\registertfm bbm  10pt-12pt bbm10
+\registertfm bbm  12pt-17pt bbm12
+\registertfm bbm  17pt-*    bbm17
+
+% \bb - blackboard bold math font
+
+\newfam\bbfam
+\def\bb{\fam\bbfam}
+
+\def\loadbbm{%
+	\loadmathfam\bbfam[/bbm]%
+}
+\loadbbm
+
+%%% Font size switches %%%
+
+% Recalculate line spacing for a given point size of the font (assuming CM-like metrics).
+% Also sets \strut and \topskip.
+\def\setbaselines#1{%
+	\dimen0=1pt
+	\dimen0=#1\dimen0
+	\normalbaselineskip=1.2\dimen0
+	\normallineskip=0.1\dimen0
+	\setbox\strutbox=\hbox{\vrule height 0.85\dimen0 depth 0.35\dimen0 width 0pt}%
+	\topskip=1\dimen0
+	\normalbaselines
+}
+
+% Switch to a specified font size (including math, line spacing etc.)
+\def\settextsize#1{%
+	\def\fomenc{CM}%
+	\setfonts[/#1]%
+	\setmath[//]%
+	\setbaselines{#1}%
+	\loadbbm
+}
+\def\twelvepoint{\settextsize{12}}
+
+%%% Various hacks %%%
+
+% Re-define \tt, so that it works in both text and math mode
+\def\tt{\ifmmode\fam\ttfam\else\setfonts[LMMono/]\fi}
+
+% Re-define ucwmac's \fontfont
+\let\footfont=\tenrm
diff --git a/mo/tex/ucwmac2.tex b/mo/tex/ucwmac2.tex
new file mode 100644
index 0000000000000000000000000000000000000000..1c29317c8073e402da2f2bca4b07597d72a03f2d
--- /dev/null
+++ b/mo/tex/ucwmac2.tex
@@ -0,0 +1,503 @@
+% The UCW Macro Collection (a successor of mjmac.tex)
+% Written by Martin Mares <mj@ucw.cz> in 2010--2018 and placed into public domain
+% -------------------------------------------------------------------------------
+
+\ifx\ucwmodule\undefined\else\endinput\fi
+
+%%% Prolog %%%
+
+% We'll use internal macros of plain TeX
+\catcode`@=11
+
+\ifx\eTeXversion\undefined
+\errmessage{ucwmac requires the e-TeX engine or its successor}
+\fi
+
+%%% PDF output detection %%%
+
+\newif\ifpdf
+\pdffalse
+
+\ifx\pdfoutput\undefined
+\else\ifnum\pdfoutput>0
+	\pdftrue
+	\pdfpkresolution=600	% Provide a reasonable default
+\fi\fi
+
+\ifx\luatexversion\undefined\else
+	% In LuaTeX \pdfpkresolution is not enough
+	\directlua{kpse.init_prog("luatex", 600, "ljfour")}
+\fi
+
+%%% Temporary registers %%%
+
+\newcount\tmpcount
+\newdimen\tmpdimen
+
+%%% Auxiliary macros %%%
+
+% Prepend/append #2 to the definition of #1
+\long\def\prependef#1#2{\expandafter\def\expandafter#1\expandafter{#2#1}}
+\long\def\appendef#1#2{\expandafter\def\expandafter#1\expandafter{#1#2}}
+
+% Variants of \def and \let, where the control sequence name is given as a string
+\def\sdef#1{\expandafter\def\csname#1\endcsname}
+\def\slet#1#2{\expandafter\let\csname#1\expandafter\endcsname\csname#2\endcsname}
+
+% Assign a control sequence given as a string, complain if it is not defined.
+\def\sget#1#2{\ifcsname#2\endcsname
+		\expandafter\let\expandafter#1\csname#2\endcsname
+	\else
+		\errmessage{Undefined control sequence #2}%
+		\let#1\relax
+	\fi
+}
+
+% Add \protected to an existing macro
+\def\addprotected#1{\protected\edef#1{\expandafter\unexpanded\expandafter{#1}}}
+
+% Protect ~
+\addprotected~
+
+\def\ucwwarn#1{\immediate\write16{*** UCWmac warning: #1 ***}}
+
+% Replace all occurrences of #1 in \tmpb by #2.
+% Thanks to Petr Olsak's OPmac for an efficient implementation.
+\bgroup \catcode`!=3 \catcode`?=3
+\gdef\replacestrings#1#2{\long\def\replacestringsA##1#1{\def\tmpb{##1}\replacestringsB}%
+   \long\def\replacestringsB##1#1{\ifx!##1\relax \else\appendef\tmpb{#2##1}%
+      \expandafter\replacestringsB\fi}%
+   \expandafter\replacestringsA\tmpb?#1!#1%
+   \long\def\replacestringsA##1?{\def\tmpb{##1}}\expandafter\replacestringsA\tmpb
+}
+\egroup
+
+%%% Page size and margins %%%
+
+% If you modify these registers, call \setuppage afterwards
+\ifx\luatexversion\undefined
+	% In LuaTeX, \pagewidth and \pageheight are primitive
+	% (also, we need \csname here, because \newdimen is \outer)
+	\csname newdimen\endcsname\pagewidth
+	\csname newdimen\endcsname\pageheight
+\fi
+\newdimen\leftmargin
+\newdimen\rightmargin
+\newdimen\topmargin
+\newdimen\bottommargin
+\newdimen\evenpageshift
+
+\def\setuppage{%
+	\hsize=\pagewidth
+	\advance\hsize by -\leftmargin
+	\advance\hsize by -\rightmargin
+	\vsize=\pageheight
+	\advance\vsize by -\topmargin
+	\advance\vsize by -\bottommargin
+	\hoffset=\leftmargin
+	\advance\hoffset by -1truein
+	\voffset=\topmargin
+	\advance\voffset by -1truein
+	\ifpdf
+		\pdfhorigin=1truein
+		\pdfvorigin=1truein
+		\ifx\luatexversion\undefined
+			\pdfpagewidth=\pagewidth
+			\pdfpageheight=\pageheight
+		\fi
+	\fi
+}
+
+% Set multiple margins to the same value
+\def\sethmargins#1{\leftmargin=#1\relax\rightmargin=#1\relax\evenpageshift=0pt\relax}
+\def\setvmargins#1{\topmargin=#1\relax\bottommargin=#1\relax}
+\def\setmargins#1{\sethmargins{#1}\setvmargins{#1}}
+
+% Define inner/outer margin instead of left/right
+\def\setinneroutermargin#1#2{\leftmargin#1\relax\rightmargin#2\relax\evenpageshift=\rightmargin\advance\evenpageshift by -\leftmargin}
+
+% Use a predefined paper format, calls \setuppage automagically
+\def\setpaper#1{%
+	\expandafter\let\expandafter\currentpaper\csname paper-#1\endcsname
+	\ifx\currentpaper\relax
+		\errmessage{Undefined paper format #1}
+	\fi
+	\currentpaper
+}
+
+% Switch to landscape orientation, calls \setuppage automagically
+\def\landscape{%
+	\dimen0=\pageheight
+	\pageheight=\pagewidth
+	\pagewidth=\dimen0
+	\setuppage
+}
+
+% Common paper sizes
+\def\defpaper#1#2#3{\expandafter\def\csname paper-#1\endcsname{\pagewidth=#2\pageheight=#3\setuppage}}
+\defpaper{a3}{297truemm}{420truemm}
+\defpaper{a4}{210truemm}{297truemm}
+\defpaper{a5}{148truemm}{210truemm}
+\defpaper{letter}{8.5truein}{11truein}
+\defpaper{legal}{8.5truein}{14truein}
+
+% Default page parameters
+\setmargins{1truein}
+\setpaper{a4}
+
+%%% Macros with optional arguments %%%
+
+% After \def\a{\withoptarg\b}, the macro \a behaves in this way:
+%	\a[arg]		does \def\optarg{arg} and then it expands \b
+%	\a		does \let\optarg=\relax and then it expands \b
+\def\withoptarg#1{\let\xoptcall=#1\futurelet\next\xopt}
+\def\xopt{\ifx\next[\expandafter\xoptwith\else\let\optarg=\relax\expandafter\xoptcall\fi}
+\def\xoptwith[#1]{\def\optarg{#1}\xoptcall}
+
+% A shortcut for defining macros with optional arguments:
+% \optdef\macro behaves as \def\domacro, while \macro itself is defined
+% as a wrapper calling \domacro using \withoptarg.
+\def\optdef#1{%
+	\edef\xoptname{\expandafter\eatbackslash\string#1}%
+	\edef#1{\noexpand\withoptarg\csname do\xoptname\endcsname}%
+	\expandafter\def\csname do\xoptname\endcsname
+}
+
+% Trick: \eatbackslash eats the next backslash of category 12
+\begingroup\lccode`\+=`\\
+\lowercase{\endgroup\def\eatbackslash+{}}
+
+% Expand to the optional argument if it exists
+\def\optargorempty{\ifx\optarg\relax\else\optarg\fi}
+
+%%% Placing material at specified coordinates %%%
+
+% Set all dimensions of a given box register to zero
+\def\smashbox#1{\ht#1=0pt \dp#1=0pt \wd#1=0pt}
+\long\def\smashedhbox#1{{\setbox0=\hbox{#1}\smashbox0\box0}}
+\long\def\smashedvbox#1{{\setbox0=\vbox{#1}\smashbox0\box0}}
+
+% Variants of \llap and \rlap working equally on both sides and/or vertically
+\def\hlap#1{\hbox to 0pt{\hss #1\hss}}
+\def\vlap#1{\vbox to 0pt{\vss #1\vss}}
+\def\clap#1{\vlap{\hlap{#1}}}
+
+% \placeat{right}{down}{hmaterial} places <hmaterial>, so that its
+% reference point lies at the given position wrt. the current ref point
+\long\def\placeat#1#2#3{\smashedhbox{\hskip #1\lower #2\hbox{#3}}}
+
+% Like \vbox, but with reference point in the upper left corner
+\long\def\vhang#1{\vtop{\hrule height 0pt\relax #1}}
+
+% Like \vhang, but respecting interline skips
+\long\def\vhanglines#1{\vtop{\hbox to 0pt{}#1}}
+
+% Crosshair with reference point in its center
+\def\crosshair#1{\clap{\vrule height 0.2pt width #1}\clap{\vrule height #1 width 0.2pt}}
+
+%%% Output routine %%%
+
+\newbox\pageunderlays
+\newbox\pageoverlays
+\newbox\commonunderlays
+\newbox\commonoverlays
+
+% In addition to the normal page contents, you can define page overlays
+% and underlays, which are zero-size vboxes positioned absolutely in the
+% front / in the back of the normal material. Also, there are global
+% versions of both which are not reset after every page.
+\def\addlay#1#2{\setbox#1=\vbox{\ifvbox#1\box#1\fi\nointerlineskip\smashedvbox{#2}}}
+\def\pageunderlay{\addlay\pageunderlays}
+\def\pageoverlay{\addlay\pageoverlays}
+\def\commonunderlay{\addlay\commonoverlays}
+\def\commonoverlay{\addlay\commonoverlays}
+
+% Our variation on \plainoutput, which manages inner/outer margins and overlays
+\output{\ucwoutput}
+\newdimen\pagebodydepth
+\def\ucwoutput{\wigglepage\shipout\vbox{%
+	\makeheadline
+	\ifvbox\commonunderlays\copy\commonunderlays\nointerlineskip\fi
+	\ifvbox\pageunderlays\box\pageunderlays\nointerlineskip\fi
+	\pagebody
+	\pagebodydepth=\prevdepth
+	\nointerlineskip
+	\ifvbox\commonoverlays\vbox to 0pt{\vskip -\vsize\copy\commonoverlays\vss}\nointerlineskip\fi
+	\ifvbox\pageoverlays\vbox to 0pt{\vskip -\vsize\box\pageoverlays\vss}\nointerlineskip\fi
+	\prevdepth=\pagebodydepth
+	\makefootline
+}\advancepageno
+\ifnum\outputpenalty>-\@MM \else\dosupereject\fi}
+
+\def\wigglepage{\ifodd\pageno\else\advance\hoffset by \evenpageshift\fi}
+
+% Make it easier to redefine footline font (also, fix it so that OFS won't change it unless asked)
+\let\footfont=\tenrm
+\footline={\hss\footfont\folio\hss}
+
+%%% Itemization %%%
+
+% Usage:
+%
+% \list{style}
+% \:first item
+% \:second item
+% \endlist
+%
+% Available styles (others can be defined by \sdef{item:<style>}{<marker>})
+%
+%	o		% bullet
+%	O		% empty circle
+%	*		% asterisk
+%	-		% en-dash
+%	.		% dot
+%	n		% 1, 2, 3
+%	i		% i, ii, iii
+%	I		% I, II, III
+%	a		% a, b, c
+%	A		% A, B, C
+%	g		% α, β, γ
+%
+% Meta-styles (can be used to modify an arbitrary style, currently hard-wired)
+%
+%	#.		% with a dot behind
+%	#)		% with a parenthesis behind
+%	(#)		% enclosed in parentheses
+%	[#]		% enclosed in square brackets
+%
+% Historic usage:
+%
+% \itemize\ibull	% or other marker
+% \:first item
+% \:second item
+% \endlist
+%
+% \numlist\ndotted	% or other numbering style
+% \:first
+% \:second
+% \endlist
+
+% Default dimensions of itemized lists
+\newdimen\itemindent		\itemindent=0.5in
+\newdimen\itemnarrow		\itemnarrow=0.5in			% make lines narrower by this amount
+\newskip\itemmarkerskip		\itemmarkerskip=0.4em			% between marker and the item
+\newskip\preitemizeskip		\preitemizeskip=3pt plus 2pt minus 1pt	% before the list
+\newskip\postitemizeskip	\postitemizeskip=3pt plus 2pt minus 1pt	% after the list
+\newskip\interitemskip		\interitemskip=2pt plus 1pt minus 0.5pt	% between two items
+
+% Analogues for nested lists
+\newdimen\nesteditemindent	\nesteditemindent=0.25in
+\newdimen\nesteditemnarrow	\nesteditemnarrow=0.25in
+\newskip\prenesteditemizeskip	\prenesteditemizeskip=0pt
+\newskip\postnesteditemizeskip	\postnesteditemizeskip=0pt
+
+\newif\ifitems\itemsfalse
+\newbox\itembox
+\newcount\itemcount
+
+% Penalties
+\newcount\preitemizepenalty	\preitemizepenalty=-500
+\newcount\postitemizepenalty	\postitemizepenalty=-500
+
+\def\preitemize{
+	\ifitems
+		\vskip\prenesteditemizeskip
+		\advance\leftskip by \nesteditemindent
+		\advance\rightskip by \nesteditemnarrow
+	\else
+		\ifnum\preitemizepenalty=0\else\penalty\preitemizepenalty\fi
+		\vskip\preitemizeskip
+		\advance\leftskip by \itemindent
+		\advance\rightskip by \itemnarrow
+	\fi
+	\parskip=\interitemskip
+}
+
+\def\postitemize{
+	\ifitems
+		\vskip\postnesteditemizeskip
+	\else
+		\ifnum\postitemizepenalty=0\else\penalty\postitemizepenalty\fi
+		\vskip\postitemizeskip
+	\fi
+}
+
+\def\inititemize{\begingroup\preitemize\itemstrue\parindent=0pt}
+
+\def\list#1{\inititemize\itemcount=0\liststyle{#1}\let\:=\listitem}
+\def\listitem{\par\leavevmode\advance\itemcount by 1
+	\llap{\listmarker\hskip\itemmarkerskip}\ignorespaces}
+
+\def\liststyle#1{%
+	\edef\markertmp{#1}
+	\ifcsname item:\markertmp\endcsname
+		\sget\listmarker{item:\markertmp}%
+	\else
+		\sget\listmarker{metaitem:\markertometa#1^^X}%
+		\sget\markerinner{item:\markertoinner#1^^X}%
+	\fi
+}
+
+\def\markertometa#1{%
+	\ifx#1^^X%
+	\else
+		\ifx#1((%
+		\else\ifx#1[[%
+		\else\ifx#1))%
+		\else\ifx#1]]%
+		\else\ifx#1..%
+		\else=%
+		\fi\fi\fi\fi\fi
+		\expandafter\markertometa
+	\fi
+}
+
+\def\markertoinner#1{%
+	\ifx#1^^X%
+	\else
+		\ifx#1(%
+		\else\ifx#1)%
+		\else\ifx#1[%
+		\else\ifx#1]%
+		\else\ifx#1.%
+		\else#1%
+		\fi\fi\fi\fi\fi
+		\expandafter\markertoinner
+	\fi
+}
+
+\def\endlist{\par\endgroup\postitemize}
+
+% List styles
+\sdef{item:o}{\raise0.2ex\hbox{$\bullet$}}
+\sdef{item:O}{\raise0.2ex\hbox{$\circ$}}
+\sdef{item:*}{\raise0.2ex\hbox{$\ast$}}
+\sdef{item:-}{--}
+\sdef{item:.}{\raise0.2ex\hbox{$\cdot$}}
+\sdef{item:n}{\the\itemcount}
+\sdef{item:i}{\romannumeral\itemcount}
+\sdef{item:I}{\uppercase\expandafter{\romannumeral\itemcount}}
+\sdef{item:a}{\char\numexpr 96+\itemcount\relax}
+\sdef{item:A}{\char\numexpr 64+\itemcount\relax}
+\sdef{item:g}{$\ifcase\itemcount\or\alpha\or\beta\or\gamma\or\delta\or\epsilon\or
+\zeta\or\eta\or\theta\or\iota\or\kappa\or\lambda\or\mu\or\nu\or\xi\or\pi\or\rho
+\or\sigma\or\tau\or\upsilon\or\phi\or\chi\or\psi\or\omega\fi$}
+
+% List meta-styles
+\sdef{metaitem:=.}{\markerinner.}
+\sdef{metaitem:=)}{\markerinner)}
+\sdef{metaitem:(=)}{(\markerinner)}
+\sdef{metaitem:[=]}{[\markerinner]}
+
+% Old-style lists
+
+\def\itemize#1{\inititemize\setbox\itembox\llap{#1\hskip\itemmarkerskip}%
+\let\:=\singleitem}
+
+\def\singleitem{\par\leavevmode\copy\itembox\ignorespaces}
+
+\def\numlist#1{\inititemize\itemcount=0\let\:=\numbereditem
+\let\itemnumbering=#1}
+
+\def\numbereditem{\par\leavevmode\advance\itemcount by 1
+\llap{\itemnumbering\hskip\itemmarkerskip}\ignorespaces}
+
+% Old-style markers
+
+\def\ibull{\raise0.2ex\hbox{$\bullet$}}
+\def\idot{\raise0.2ex\hbox{$\cdot$}}
+\def\istar{\raise0.2ex\hbox{$\ast$}}
+
+\def\nnorm{\the\itemcount}
+\def\ndotted{\nnorm.}
+\def\nparen{\nnorm)}
+\def\nparenp{(\nnorm)}
+\def\nroman{\romannumeral\itemcount}
+\def\nromanp{\nroman)}
+\def\nalpha{\count@=96\advance\count@ by\itemcount\char\count@)}
+\def\nAlpha{\count@=64\advance\count@ by\itemcount\char\count@)}
+\def\ngreek{$\ifcase\itemcount\or\alpha\or\beta\or\gamma\or\delta\or\epsilon\or
+\zeta\or\eta\or\theta\or\iota\or\kappa\or\lambda\or\mu\or\nu\or\xi\or\pi\or\rho
+\or\sigma\or\tau\or\upsilon\or\phi\or\chi\or\psi\or\omega\fi$)}
+
+%%% Miscellanea %%%
+
+% {\I italic} with automatic italic correction
+\def\I{\it\aftergroup\/}
+
+% A breakable dash, to be repeated on the next line
+\def\={\discretionary{-}{-}{-}}
+
+% Non-breakable identifiers
+\def\<#1>{\leavevmode\hbox{\I #1}}
+
+% Handy shortcuts
+\let\>=\noindent
+\def\\{\hfil\break}
+
+% Variants of \centerline, \leftline and \rightline, which are compatible with
+% verbatim environments and other catcode hacks
+\def\cline{\bgroup\def\linet@mp{\aftergroup\box\aftergroup0\aftergroup\egroup\hss\bgroup\aftergroup\hss\aftergroup\egroup}\afterassignment\linet@mp\setbox0\hbox to \hsize}
+\def\lline{\bgroup\def\linet@mp{\aftergroup\box\aftergroup0\aftergroup\egroup\bgroup\aftergroup\hss\aftergroup\egroup}\afterassignment\linet@mp\setbox0\hbox to \hsize}
+\def\rline{\bgroup\def\linet@mp{\aftergroup\box\aftergroup0\aftergroup\egroup\hss\bgroup\aftergroup\egroup}\afterassignment\linet@mp\setbox0\hbox to \hsize}
+
+% Insert a PDF picture
+% \putimage{width specification}{file}
+\def\putimage#1#2{\hbox{\pdfximage #1{#2}\pdfrefximage\pdflastximage}}
+
+%%% Colors %%%
+
+% Use of pdfTeX color stack:
+% \colorpush\rgb{1 0 0} puts a new color on the stack
+% \colorset\rgb{1 0 0} replaces the top color on the stack
+% \colorpop pops the top color
+% \colorlocal\rgb{1 0 0} set a color locally until the end of the current group
+\chardef\colorstk=\pdfcolorstackinit page direct{0 g 0 G}
+\def\colorset#1{\pdfcolorstack\colorstk set #1}
+\def\colorpush#1{\pdfcolorstack\colorstk push #1}
+\def\colorpop{\pdfcolorstack\colorstk pop}
+\def\colorlocal{\aftergroup\colorpop\colorpush}
+
+% Different ways of describing colors: \rgb{R G B}, \gray{G}, \cmyk{C M Y K}
+% (all components are real numbers between 0 and 1)
+\def\rgb#1{{#1 rg #1 RG}}
+\def\gray#1{{#1 g #1 G}}
+\def\cmyk#1{{#1 k #1 K}}
+
+%%% Localization %%%
+
+% Define a new localized string: \localedef{language}{identifier}{message}
+% (we use \language codes to identify languages)
+\def\localedef#1#2{\tmpcount=#1\expandafter\def\csname loc:\the\tmpcount:#2\endcsname}
+
+% Expand a localized string in the current language: \localemsg{identifier}
+\def\localestr#1{%
+	\ifcsname loc:\the\language:#1\endcsname
+		\csname loc:\the\language:#1\endcsname
+	\else
+		\ucwwarn{Localized string #1 not defined in language \the\language}%
+		???%
+	\fi
+}
+
+%%% Modules %%%
+
+% Require a module: load it if it is not already loaded
+\def\ucwmodule#1{
+	\ifcsname ucwmod:#1\endcsname
+	\else
+		\input ucw-#1.tex
+	\fi
+}
+
+% Definition of a new module (to be placed at the beginning of its file)
+% (Also guards against repeated loading if somebody uses \input instead of \ucwmodule.)
+\def\ucwdefmodule#1{
+	\ifcsname ucwmod:#1\endcsname\endinput\fi
+	\expandafter\let\csname ucwmod:#1\endcsname=\relax
+}
+
+%%% Epilog %%%
+
+% Let's hide all internal macros
+\catcode`@=12
diff --git a/mo/util.py b/mo/util.py
index 3b52844c7c900c147d3f8b83181f00490af2948d..dab552fbab7e7fd7d9bdf7fb889a05f036cdefda 100644
--- a/mo/util.py
+++ b/mo/util.py
@@ -89,6 +89,11 @@ def data_dir(name: str) -> str:
     return os.path.join(config.DATA_DIR, name)
 
 
+def part_path(name: str) -> str:
+    """Vrátí cestu k datovém souboru, který se instaluje jako součást pythoních modulů."""
+    return os.path.normpath(os.path.join(__file__, "..", name))
+
+
 def link_to_dir(src: str, dest_dir: str, prefix: str = "", suffix: str = "") -> str:
     """Vytvoří hardlink na zdrojový soubor pod unikátním jménem v cílovém adresáři."""
 
diff --git a/mo/web/org_contest.py b/mo/web/org_contest.py
index 823c12bbf2f7782ef0802b515c3b98c8c2234900..4a70b4cafa082be95202151ca0c2e36e1444957f 100644
--- a/mo/web/org_contest.py
+++ b/mo/web/org_contest.py
@@ -18,6 +18,7 @@ import mo
 import mo.config as config
 import mo.db as db
 from mo.imports import PointsImport, ContestImport, OrgsImport
+import mo.jobs.protocols
 import mo.jobs.submit
 from mo.rights import Right, RoundRights
 import mo.util
@@ -1163,7 +1164,7 @@ class DownloadSubmitsForm(FlaskForm):
     download_fb_mix = wtforms.SubmitField('Stáhnout opravená/účastnická')
 
 
-def download_submits(form: DownloadSubmitsForm, round: db.Round, sol_query, pion_query, subj_suffix: str, want_subdirs: bool) -> bool:
+def download_submits(form: DownloadSubmitsForm, round: db.Round, sol_query, pion_query, out_name: str, subj_suffix: str, want_subdirs: bool) -> bool:
     if not form.validate_on_submit():
         return False
 
@@ -1196,7 +1197,7 @@ def download_submits(form: DownloadSubmitsForm, round: db.Round, sol_query, pion
         return False
 
     paper_ids = [p for p in paper_ids if p is not None]
-    mo.jobs.submit.schedule_download_submits(paper_ids, f'{subj_prefix} {subj_suffix}', g.user, want_subdirs)
+    mo.jobs.submit.schedule_download_submits(paper_ids, f'{subj_prefix} {subj_suffix}', g.user, want_subdirs, out_name)
     flash('Příprava řešení ke stažení zahájena.', 'success')
     return True
 
@@ -1236,7 +1237,8 @@ def org_generic_batch_download(task_id: int, round_id: Optional[int] = None, hie
             subj = f'{subj} ({contest.place.name})'
         elif hier_place is not None:
             subj = f'{subj} ({hier_place.name})'
-        if download_submits(form, round, sol_query, pion_query, subj, contest is None):
+        out_name = f'reseni_{task.code}'
+        if download_submits(form, round, sol_query, pion_query, out_name, subj, contest is None):
             return redirect(url_for('org_jobs'))
 
     sol_paper = aliased(db.Paper)
@@ -1628,3 +1630,79 @@ def org_contest_add_user(ct_id: int, site_id: Optional[int] = None):
         contest=contest, round=ctx.master_round, site=ctx.site,
         form=form
     )
+
+
+class GenProtoForm(FlaskForm):
+    num_universal = wtforms.IntegerField(
+        'Univerzálních listů',
+        default=0,
+        validators=[validators.NumberRange(min=0, max=1000)],
+        description='Počet listů s univerzální hlavičkou, kterými může začínat řešení libovolné úlohy. ' +
+                    'Při scanování se třídí ručně.'
+    )
+    num_blank = wtforms.IntegerField(
+        'Pokračovacích listů',
+        default=0,
+        validators=[validators.NumberRange(min=0, max=1000)],
+        description='Počet listů na pokračování řešení.',
+    )
+    gen_protos = wtforms.SubmitField('Vytvořit protokoly')
+
+
+class ProcessScansForm(FlaskForm):
+    files = wtforms.MultipleFileField('Soubory PDF se scany', validators=[validators.required()])
+    process_scans = wtforms.SubmitField('Zpracovat scany')
+
+
+@app.route('/org/contest/c/<int:ct_id>/protocols', methods=('GET', 'POST'))
+@app.route('/org/contest/c/<int:ct_id>/site/<int:site_id>/protocols', methods=('GET', 'POST'))
+def org_contest_protocols(ct_id: int, site_id: Optional[int] = None):
+    ctx = get_context(ct_id=ct_id, site_id=site_id)
+    round, contest, site = ctx.round, ctx.contest, ctx.site
+    assert contest
+
+    class GPF(GenProtoForm):
+        pass
+
+    class PSF(ProcessScansForm):
+        pass
+
+    tasks = db.get_session().query(db.Task).filter_by(round=round).order_by(db.Task.code).all()
+    for t in tasks:
+        setattr(GPF, f'task_{t.task_id}', wtforms.BooleanField(t.code, default=True))
+        setattr(PSF, f'task_{t.task_id}', wtforms.BooleanField(t.code, default=True))
+
+    gen_form = GPF()
+    gen_task_fields = [f for f in gen_form if f.name.startswith('task_')]
+
+    proc_form = PSF()
+    proc_task_fields = [f for f in proc_form if f.name.startswith('task_')]
+
+    if gen_form.validate_on_submit() and gen_form.gen_protos.data:
+        mo.jobs.protocols.schedule_create_protocols(
+            contest, site, g.user,
+            tasks=[t for t in tasks if getattr(gen_form, f'task_{t.task_id}').data],
+            num_universal=gen_form.num_universal.data,
+            num_blank=gen_form.num_blank.data,
+        )
+        flash('Výroba prototokolů zahájena.', 'success')
+        return redirect(url_for('org_jobs'))
+
+    if proc_form.validate_on_submit() and proc_form.process_scans.data:
+        files = request.files.getlist(proc_form.files.name)
+        mo.jobs.protocols.schedule_process_scans(
+            contest, site, g.user,
+            tasks=[t for t in tasks if getattr(proc_form, f'task_{t.task_id}').data],
+            in_file_names=[f.stream.name for f in files],
+        )
+        flash('Zpracování scanů zahájeno.', 'success')
+        return redirect(url_for('org_jobs'))
+
+    return render_template(
+        'org_contest_protocols.html',
+        ctx=ctx,
+        gen_form=gen_form,
+        gen_task_fields=gen_task_fields,
+        proc_form=proc_form,
+        proc_task_fields=proc_task_fields,
+    )
diff --git a/mo/web/org_jobs.py b/mo/web/org_jobs.py
index 2fedcf41e79a6616601b5cd95caf96ff5d240519..8cf9786e86bdc9f7d335dd69da3cec78ddd83808 100644
--- a/mo/web/org_jobs.py
+++ b/mo/web/org_jobs.py
@@ -1,12 +1,14 @@
 from flask import render_template, g, redirect, url_for, flash
 from flask_wtf.form import FlaskForm
+import os
 from sqlalchemy.orm import joinedload
+from typing import Optional
 import werkzeug.exceptions
 import wtforms
 
 import mo
 import mo.db as db
-from mo.jobs import TheJob, job_file_size
+from mo.jobs import TheJob
 from mo.web import app
 import mo.web.util
 
@@ -60,6 +62,16 @@ def get_job(id: int) -> db.Job:
     return job
 
 
+def job_file_size(job: db.Job, name: Optional[str]) -> Optional[int]:
+    if name is None:
+        return None
+
+    try:
+        return os.path.getsize(job.file_path(name))
+    except OSError:
+        return -1
+
+
 @app.route('/org/jobs/<int:id>/')
 def org_job(id: int):
     job = get_job(id)
@@ -72,8 +84,8 @@ def org_job(id: int):
         'org_job.html',
         job=job,
         has_errors=has_errors,
-        in_size=job_file_size(job.in_file),
-        out_size=job_file_size(job.out_file),
+        in_size=job_file_size(job, job.in_file),
+        out_size=job_file_size(job, job.out_file),
     )
 
 
diff --git a/mo/web/templates/org_contest.html b/mo/web/templates/org_contest.html
index 9fa6826d00312dd9620ba10d6ff2250918ffc37a..53d44d0640d2452d1cfcb878d57250e14bd6a5b5 100644
--- a/mo/web/templates/org_contest.html
+++ b/mo/web/templates/org_contest.html
@@ -60,6 +60,7 @@
 	{% if state in [RoundState.grading, RoundState.closed] %}
 	<a class="btn btn-primary" href='{{ ctx.url_for('org_score') }}'>Výsledky</a>
 	{% endif %}
+	<a class="btn btn-default" href='{{ ctx.url_for('org_contest_protocols') }}'>Protokoly</a>
 	{% if state == RoundState.preparing and round.seq > 1 %}
 	<a class="btn btn-primary" href='{{ ctx.url_for('org_contest_advance') }}'>Postup z minulého kola</a>
 	{% endif %}
diff --git a/mo/web/templates/org_contest_protocols.html b/mo/web/templates/org_contest_protocols.html
new file mode 100644
index 0000000000000000000000000000000000000000..cf1b8a53b846881d538f9d2b484bfebf29a254d8
--- /dev/null
+++ b/mo/web/templates/org_contest_protocols.html
@@ -0,0 +1,56 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% block title %}
+Protokoly pro {{ ctx.round.name|lower }} kategorie {{ ctx.round.category }}
+{% endblock %}
+{% block breadcrumbs %}
+{{ ctx.breadcrumbs(action="Protokoly") }}
+{% endblock %}
+{% block body %}
+
+<p>Zde je možné vytvořit PDF s formuláři protokolů pro všechny soutěžící. Každý
+formulář je opatřen unikátním QR kódem. FIXME: Dovysvětlit.
+
+<h3>Formuláře protokolů</h3>
+
+{% macro field(f) %}
+{{ wtf.form_field(f, form_type='horizontal', horizontal_columns=('lg', 3, 7), button_map={'gen_protos': 'primary', 'process_scans': 'primary'}) }}
+{% endmacro %}
+
+<form action="" method=POST class="form form-horizontal" role="form">
+	{{ gen_form.csrf_token }}
+	{% if gen_task_fields %}
+		<div class='form-group'>
+			<label class='control-label col-lg-3' for='{{ gen_task_fields[0].id }}'>Úlohy</label>
+			<div class='col-lg-7'>
+				{% for f in gen_task_fields %}
+					{{ wtf.form_field(f) }}
+				{% endfor %}
+			</div>
+		</div>
+	{% endif %}
+	{{ field(gen_form.num_universal) }}
+	{{ field(gen_form.num_blank) }}
+	{{ field(gen_form.gen_protos) }}
+</form>
+
+<h3>Zpracování scanů</h3>
+
+<form action="" method=POST class="form form-horizontal" role="form" enctype='multipart/form-data'>
+	{{ proc_form.csrf_token }}
+	{% if proc_task_fields %}
+		<div class='form-group'>
+			<label class='control-label col-lg-3' for='{{ proc_task_fields[0].id }}'>Úlohy</label>
+			<div class='col-lg-7'>
+				{% for f in proc_task_fields %}
+					{{ wtf.form_field(f) }}
+				{% endfor %}
+			</div>
+		</div>
+	{% endif %}
+	{{ field(proc_form.files) }}
+	{{ field(proc_form.process_scans) }}
+</form>
+
+{% endblock %}
diff --git a/mo/web/util.py b/mo/web/util.py
index 1be8eb8027ee958df4216f8bde64904d9e1f2938..90b9bfa9bd988a083031e7ac24f9378ddb4857b8 100644
--- a/mo/web/util.py
+++ b/mo/web/util.py
@@ -113,10 +113,12 @@ def send_task_paper(paper: db.Paper, orig: bool = False) -> Response:
 
 def send_job_result(job: db.Job) -> Response:
     assert job.out_file is not None
-    file = mo.jobs.job_file_path(job.out_file)
+    file = job.file_path(job.out_file)
 
     if file.endswith('.zip'):
         type = 'application/zip'
+    elif file.endswith('.pdf'):
+        type = 'application/pdf'
     else:
         type = 'application/binary'
 
diff --git a/setup.py b/setup.py
index 8398fe3e8eeb1214c5eeeedf4a4b0de8b34b70d3..07bbeabc137f6b2f7a5ee2074fbf8d1035eb3311 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,10 @@ setuptools.setup(
         'flask_sqlalchemy',
         'markdown',
         'pikepdf',
+        'pillow',
         'psycopg2',
+        'python-poppler',
+        'pyzbar',
         'sqlalchemy',
         'uwsgidecorators',
     ],