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', ],