feat: refine repeated dual-QR header

This commit is contained in:
user
2026-07-16 10:11:55 +02:00
parent 462ce93968
commit c40ea35fc3
2 changed files with 318 additions and 209 deletions
+5 -1
View File
@@ -84,7 +84,7 @@ def check_render() -> None:
tex = tex_path.read_text(encoding="utf-8")
html = html_path.read_text(encoding="utf-8")
for marker in ("DEMO-01", "IP WO", "INF04", "KARTA PRACY"):
for marker in ("DEMO-01", "IP WO", "INF04", "System kart pracy"):
if marker not in tex:
raise AssertionError(f"TeX nie zawiera znacznika {marker!r}")
if marker not in html:
@@ -104,6 +104,10 @@ def check_render() -> None:
for marker in ("resource-qr-repository", "resource-qr-page"):
if marker not in html:
raise AssertionError(f"HTML nie zawiera komórki {marker!r}")
if "PROJECT" not in html:
raise AssertionError("HTML nie zawiera nowej tabliczki projektu")
if "resource-qr-label" in html:
raise AssertionError("HTML nadal zawiera podpis pod kodem QR")
for marker in ('class="title-qr"', 'class="code-subblock"'):
if marker not in html:
raise AssertionError(f"HTML nie zawiera znacznika {marker!r}")
+310 -205
View File
@@ -673,9 +673,6 @@ def macro(name: str, value: str) -> str:
def render_preamble(data: dict) -> list[str]:
card = data["card"]
header_height_mm = resource_header_height_mm(data)
source_url = repository_url(data)
doc_uuid = str((data.get("title_block") or {}).get("doc_uuid", ""))
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
return [
r"\documentclass[12pt,a4paper]{article}",
"",
@@ -687,7 +684,7 @@ def render_preamble(data: dict) -> list[str]:
r"\usepackage{amsmath}",
r"\usepackage{amssymb}",
r"\usepackage{xcolor}",
rf"\usepackage[a4paper,left=2.15cm,right=2.15cm,top=0.5cm,bottom=0.5cm,headheight={header_height_mm:g}mm,headsep=3mm,footskip=8mm,includehead,includefoot]{{geometry}}",
rf"\usepackage[a4paper,left=2.15cm,right=2.15cm,top=0.5cm,bottom=0.5cm,headheight={header_height_mm:g}mm,headsep=3mm,includehead]{{geometry}}",
r"\usepackage{eso-pic}",
r"\usepackage{marginnote}",
r"\usepackage{array}",
@@ -742,16 +739,6 @@ def render_preamble(data: dict) -> list[str]:
r"\pagestyle{fancy}",
r"\fancyhf{}",
r"\fancyhead[C]{\ESCPageResourceHeader}",
r"\fancyfoot[C]{%",
r" \begin{minipage}{\textwidth}%",
r" \hrule height0.35pt\vspace{0.5mm}%",
r" \fontsize{4.2}{4.8}\selectfont\ttfamily%",
r" \begin{tabularx}{\textwidth}{@{}p{27mm}p{49mm}Xr@{}}%",
rf" \textbf{{KARTA}} {tex_escape(card_number)} & \textbf{{UUID}} {tex_escape(card.get('uuid', ''))} & \textbf{{DOKUMENT}} {tex_escape(doc_uuid)} & \textbf{{ARKUSZ}} \thepage/\pageref{{LastPage}} \\%",
rf" \textbf{{WERSJA}} {tex_escape(card.get('version', ''))} · {tex_escape(card.get('revision_date', ''))} & \multicolumn{{2}}{{l}}{{\fontsize{{3.6}}{{4.1}}\selectfont\textbf{{GITEA}} \href{{{source_url}}}{{\nolinkurl{{{source_url}}}}}}} & \textbf{{AUTOR}} {tex_escape(card.get('author', ''))}%",
r" \end{tabularx}%",
r" \end{minipage}%",
r"}",
r"\renewcommand{\headrulewidth}{0pt}",
r"\renewcommand{\footrulewidth}{0pt}",
"",
@@ -3100,9 +3087,69 @@ def title_block_fields(data: dict) -> dict[str, str | bool]:
def resource_header_height_mm(data: dict) -> float:
"""Return the configured repeated-header height in millimetres."""
"""Return header height after removing the former QR-caption strip."""
configured = data.get("title_block") or {}
return float(configured.get("height_cm", 3)) * 10
configured_height = float(configured.get("height_cm", 3)) * 10
return max(23.0, configured_height - 2.0)
def resource_header_level_values(card: dict) -> tuple[str, str, str, str, str, str]:
"""Split the curriculum code into the compact third-row fields."""
parts = [part.strip() for part in re.split(r"\s*[·|]\s*", str(card.get("level", ""))) if part.strip()]
if len(parts) >= 5:
program, separator, ko_date = parts[0].partition("@")
subject = parts[1]
scope = parts[2]
level = re.sub(r"(?i)^kl\.\s*", "", parts[3]).strip()
position = re.sub(r"(?i)^poz\.\s*", "", parts[4]).strip()
return subject, program, ko_date if separator else "", scope, level, position
subject = "Inf." if str(card.get("subject", "")).lower().startswith("inf") else str(card.get("subject", ""))
return subject, "", "", "", "", ""
def resource_header_layout(data: dict, sheet_value: str) -> list[list[dict]]:
"""Describe the four-row, 119 mm center grid shared by all renderers."""
card = data["card"]
fields = title_block_fields(data)
subject, program, ko_date, scope, level, position = resource_header_level_values(card)
configured = data.get("title_block") or {}
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
return [
[
{"width": 59.5, "label": "TITLE", "value": card.get("title", ""), "title": True},
{"width": 18, "label": "VER.", "value": fields.get("revision", card.get("version", "")), "emphasis": True},
{"width": 41.5, "label": "DATETIME", "value": fields.get("prepared_on", card.get("revision_date", ""))},
],
[
{"width": 59.5, "label": "PROJECT", "value": card.get("project", ""), "title": True},
{"width": 30, "label": "SERIES", "value": card.get("series_title", card.get("series", ""))},
{"width": 17, "label": "CARD", "value": card_number},
{"width": 12.5, "label": "SHEET", "value": sheet_value, "key": "sheet"},
],
[
{"width": 7, "label": "SUBJ.", "value": subject, "compact": True},
{"width": 6, "label": "PROG.", "value": program, "compact": True},
{"width": 17.5, "label": "CORE", "value": ko_date, "compact": True},
{"width": 11, "label": "SCOPE", "value": scope, "compact": True},
{"width": 9, "label": "LEVEL", "value": level, "compact": True},
{"width": 9, "label": "POS.", "value": position, "compact": True},
{
"width": 59.5,
"kind": "identity",
"uuid_gitea": str(configured.get("url_host_uuid", card.get("uuid", ""))),
"uuid_card": str(configured.get("doc_uuid", card.get("uuid", ""))),
"author": card.get("author", ""),
},
],
[
{
"width": 119,
"kind": "urls",
"repository_url": str(fields.get("repository_url", "")),
"url": str(fields.get("url", "")),
},
],
]
@lru_cache(maxsize=32)
@@ -3219,7 +3266,7 @@ def render_page_header_html(data: dict, page: int, total: int) -> str:
return (
f'<a class="{classes}" href="{html_escape(target)}" '
f'aria-label="{html_escape(label)}: otwórz {html_escape(target)}">'
f'{qr}<span class="resource-qr-label">{html_escape(label)}</span></a>'
f'{qr}</a>'
)
return (
f'<div class="{classes} resource-qr-empty">'
@@ -3233,103 +3280,95 @@ def render_page_header_html(data: dict, page: int, total: int) -> str:
else ""
)
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
sheet_number = f"{page}/{total}"
header_class = "page-resource-header page-resource-header--dual" if repository_qr_html else "page-resource-header"
layout = resource_header_layout(data, sheet_number)
def inline_fields(items: list[tuple[str, object]], strong_first: bool = False) -> str:
rendered: list[str] = []
for index, (label, value) in enumerate(items):
if index:
rendered.append('<span class="resource-separator">|</span>')
rendered.append(f'<span class="resource-label">{html_escape(label)}</span>')
tag = "strong" if strong_first and index == 0 else "span"
rendered.append(f'<{tag} class="resource-value">{html_escape(value or "")}</{tag}>')
return '<div class="resource-inline">' + "".join(rendered) + "</div>"
primary_rows = [
inline_fields(
[
(fields["category"], f"{card_prefix(card)}: {card.get('title', '')}"),
],
strong_first=True,
),
inline_fields(
[
("Projekt", card.get("project", "")),
("Seria", card.get("series_title", card.get("series", ""))),
]
),
inline_fields(
[
("Przedmiot", card.get("subject", "")),
("Poziom", card.get("level", "")),
]
),
inline_fields([("HTML", url or "brak URL")]),
]
meta_rows = [
inline_fields(
[("Data", card.get("revision_date", "")), ("Status", card.get("status", ""))]
),
inline_fields([("Karta", card_number), ("Wersja", card.get("version", ""))]),
inline_fields([("Autor", card.get("author", "")), ("Arkusz", sheet_number)]),
inline_fields([("UUID", card.get("uuid", ""))]),
]
column_group = (
'<col style="width:24mm"><col><col style="width:39mm"><col style="width:24mm">'
if repository_qr_html
else '<col><col style="width:47mm"><col style="width:24mm">'
def render_field(field: dict) -> str:
kind = str(field.get("kind", ""))
if kind == "identity":
return (
'<div class="resource-header-field resource-header-field--identity">'
'<span><b>GITEA UUID</b> '
f'{html_escape(field.get("uuid_gitea", ""))}</span>'
'<span><b>CARD UUID</b> '
f'{html_escape(field.get("uuid_card", ""))}'
f'<b class="resource-author">AUTHOR&nbsp; {html_escape(field.get("author", ""))}</b></span>'
'</div>'
)
if kind == "urls":
repository_target = str(field.get("repository_url", ""))
html_target = str(field.get("url", ""))
return (
'<div class="resource-header-field resource-header-field--urls">'
f'<a href="{html_escape(repository_target)}"><b>GITEA</b>&nbsp; {html_escape(repository_target)}</a>'
f'<a href="{html_escape(html_target)}"><b>HTML</b>&nbsp; {html_escape(html_target)}</a>'
'</div>'
)
classes = ["resource-header-field"]
if field.get("title"):
classes.append("resource-header-field--title")
if field.get("url"):
classes.append("resource-header-field--url")
if field.get("emphasis"):
classes.append("resource-header-field--emphasis")
if field.get("compact"):
classes.append("resource-header-field--compact")
label = str(field.get("label", ""))
value = str(field.get("value", "") or "")
label_html = f'<span class="resource-label">{html_escape(label)}</span>' if label else ""
value_html = f'<span class="resource-value">{html_escape(value)}</span>'
content = label_html + value_html
href = str(field.get("href", ""))
if href:
content = (
f'<a href="{html_escape(href)}" title="{html_escape(href)}">'
f'{content}</a>'
)
return f'<div class="{" ".join(classes)}">{content}</div>'
def render_row(row: list[dict]) -> str:
columns = " ".join(f'{field["width"]}fr' for field in row)
return (
f'<div class="resource-header-row" style="grid-template-columns:{columns}">'
f'{"".join(render_field(field) for field in row)}</div>'
)
center_rows = "".join(render_row(row) for row in layout)
column_group = (
'<col style="width:24mm"><col><col style="width:24mm">'
if repository_qr_html
else '<col><col style="width:24mm">'
)
left_cell = (
f'<td class="resource-qr-cell resource-qr-cell--left">{repository_qr_html}</td>'
if repository_qr_html
else ""
)
rows: list[str] = []
for index, (primary, meta) in enumerate(zip(primary_rows, meta_rows)):
cells: list[str] = []
if index == 0 and repository_qr_html:
cells.append(f'<td rowspan="4" class="resource-qr-cell resource-qr-cell--left">{repository_qr_html}</td>')
cells.append(f'<td class="resource-header-cell resource-header-cell--primary">{primary}</td>')
cells.append(f'<td class="resource-header-cell resource-header-cell--meta">{meta}</td>')
if index == 0:
cells.append(f'<td rowspan="4" class="resource-qr-cell resource-qr-cell--right">{page_qr_html}</td>')
rows.append("<tr>" + "".join(cells) + "</tr>")
return (
f'<header class="{header_class}" data-resource-url="{html_escape(url)}" '
f'data-repository-url="{html_escape(source_url)}" '
f'data-page="{page}" data-page-total="{total}">'
f'<table class="resource-header-table"><colgroup>{column_group}</colgroup>'
f'<tbody>{"".join(rows)}</tbody></table>'
f'<tbody><tr>{left_cell}<td class="resource-header-center">'
f'<div class="resource-header-grid">{center_rows}</div></td>'
f'<td class="resource-qr-cell resource-qr-cell--right">{page_qr_html}</td>'
f'</tr></tbody></table>'
'</header>'
)
def render_page_footer_html(data: dict, page: int, total: int) -> str:
"""Render the compact per-sheet footer used by the HTML A4 preview."""
card = data["card"]
title_block = data.get("title_block") or {}
doc_uuid = str(title_block.get("doc_uuid", ""))
source_url = repository_url(data)
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
return (
'<footer class="page-resource-footer" role="contentinfo"><table><tbody>'
'<tr>'
f'<td><b>KARTA</b> {html_escape(card_number)}</td>'
f'<td><b>UUID</b> {html_escape(card.get("uuid", ""))}</td>'
f'<td><b>DOKUMENT</b> {html_escape(doc_uuid)}</td>'
f'<td class="footer-right"><b>ARKUSZ</b> {page}/{total}</td>'
'</tr><tr>'
f'<td><b>WERSJA</b> {html_escape(card.get("version", ""))} · {html_escape(card.get("revision_date", ""))}</td>'
f'<td colspan="2"><b>GITEA</b> {html_escape(source_url)}</td>'
f'<td class="footer-right"><b>AUTOR</b> {html_escape(card.get("author", ""))}</td>'
'</tr></tbody></table></footer>'
)
"""The compact resource header carries all repeated metadata."""
return ""
def page_resource_header_css(data: dict) -> str:
return r"""
.page-resource-header{width:100%;height:__RESOURCE_HEADER_HEIGHT__;min-height:__RESOURCE_HEADER_HEIGHT__;max-height:__RESOURCE_HEADER_HEIGHT__;background:#fff;color:#111;overflow:hidden;font-family:ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;line-height:1.08}
.resource-header-table{width:100%;height:100%;border-collapse:collapse;table-layout:fixed;border:1px solid #111;font:600 5pt/1.05 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace}.resource-header-table tr{height:25%}.resource-header-cell{padding:.25mm .65mm;border-right:1px solid #777;border-bottom:1px solid #aaa;vertical-align:middle;overflow:hidden}.resource-header-table tr:last-child .resource-header-cell{border-bottom:0}.resource-header-cell--meta{border-right:1px solid #777}.resource-inline{display:flex;align-items:baseline;gap:.55mm;min-width:0;white-space:nowrap;overflow:hidden}.resource-label{font-weight:700;text-transform:uppercase;flex:none}.resource-value{font:inherit;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resource-header-table tr:first-child .resource-header-cell--primary .resource-value{font-weight:800;font-size:6pt}.resource-separator{color:#999;flex:none}
.resource-qr-cell{padding:0;vertical-align:middle;overflow:hidden}.resource-qr-cell--left{border-right:1px solid #777}.resource-qr-cell--right{border-left:1px solid #777}.resource-qr{box-sizing:border-box;width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.25mm;padding:.45mm;color:#333;text-decoration:none}.resource-qr .title-qr{display:block;width:22mm;height:22mm;max-width:22mm;max-height:22mm;flex:none}.resource-qr-label{font:700 4.5pt/1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;letter-spacing:.08em;color:#333}.resource-qr-empty{text-align:center}.resource-qr-empty span,.resource-qr-empty strong{display:block;font:600 5pt/1.15 ui-monospace,SFMono-Regular,Consolas,monospace}
.page-resource-footer{position:absolute;left:21.5mm;right:21.5mm;bottom:5mm;border-top:1px solid #111;padding-top:.6mm;font:500 5pt/1.1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;color:#222;overflow:hidden}.page-resource-footer table{width:100%;border-collapse:collapse;table-layout:fixed;font:inherit}.page-resource-footer td{width:25%;padding:.25mm .6mm;border:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.page-resource-footer td+td{border-left:1px solid #ddd}.page-resource-footer .footer-right{text-align:right}
.resource-header-table{width:100%;height:100%;border-collapse:collapse;table-layout:fixed;border:1px solid #111}.resource-header-table>tbody>tr{height:100%}.resource-header-center{height:100%;padding:0;vertical-align:middle;overflow:hidden}.resource-header-grid{display:grid;grid-template-rows:1fr 1fr 1fr 2fr;width:100%;height:100%}.resource-header-row{display:grid;min-width:0;min-height:0}.resource-header-field{display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:.08mm;min-width:0;padding:.12mm .55mm;border-right:1px solid #888;border-bottom:1px solid #aaa;overflow:hidden;white-space:nowrap}.resource-header-row .resource-header-field:last-child{border-right:0}.resource-header-row:last-child .resource-header-field{border-bottom:0}.resource-header-field a{display:flex;flex-direction:column;align-items:flex-start;gap:.08mm;min-width:0;color:#111;text-decoration:none}.resource-label{font:800 2.8pt/1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;text-transform:uppercase;flex:none}.resource-value{font:700 5.6pt/1 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.resource-header-field--title .resource-value{font-size:6.8pt;font-weight:800}.resource-header-field--emphasis .resource-value{font-size:5.8pt;font-weight:800}.resource-header-field--compact{gap:.05mm;padding-inline:.18mm}.resource-header-field--compact .resource-label{font-size:2.5pt}.resource-header-field--compact .resource-value{font-size:3.9pt}.resource-header-field--identity{display:flex;flex-direction:column;align-items:stretch;justify-content:center;gap:.1mm;padding:.1mm .45mm;font:700 3.25pt/1.12 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace}.resource-header-field--identity span{display:flex;align-items:baseline;gap:.3mm;min-width:0}.resource-header-field--identity b{font-weight:800}.resource-header-field--identity .resource-author{margin-left:auto;text-align:right}.resource-header-field--urls{display:flex;flex-direction:column;align-items:stretch;justify-content:center;gap:.35mm;padding:.3mm .55mm}.resource-header-field--urls a{display:block;overflow:hidden;text-overflow:clip;font:700 4.6pt/1.05 ui-monospace,SFMono-Regular,"Latin Modern Mono",Consolas,monospace}.resource-header-field--urls b{font-weight:800}
.resource-qr-cell{height:100%;padding:0;vertical-align:middle;overflow:hidden}.resource-qr-cell--left{border-right:1px solid #777}.resource-qr-cell--right{border-left:1px solid #777}.resource-qr{box-sizing:border-box;width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:.4mm;color:#333;text-decoration:none}.resource-qr .title-qr{display:block;width:22mm;height:22mm;max-width:22mm;max-height:22mm;flex:none}.resource-qr-empty{text-align:center}.resource-qr-empty span,.resource-qr-empty strong{display:block;font:600 5pt/1.15 ui-monospace,SFMono-Regular,Consolas,monospace}
""".replace("__RESOURCE_HEADER_HEIGHT__", f"{resource_header_height_mm(data):g}mm")
@@ -3362,98 +3401,110 @@ def page_resource_print_css(data: dict) -> str:
page_qr_background = qr_background(url, bool(fields["show_qr"]))
repository_qr_enabled = bool(fields["show_repository_qr"])
repository_qr_background = qr_background(source_url, repository_qr_enabled)
sheet_token = "§"
print_layout = resource_header_layout(data, sheet_token)
row_units = [1, 1, 1, 2]
unit_height_mm = header_height_mm / sum(row_units)
row_starts_mm: list[float] = []
current_y_mm = 0.0
for units in row_units:
row_starts_mm.append(current_y_mm)
current_y_mm += unit_height_mm * units
grid_paths = [
f"M0 {sum(row_units[:row_index]) * unit_height_mm:g}H119"
for row_index in range(1, 4)
]
for row_index, row in enumerate(print_layout):
boundary = 0.0
for field in row[:-1]:
boundary += float(field["width"])
grid_paths.append(
f"M{boundary:g} {row_starts_mm[row_index]:g}V{row_starts_mm[row_index] + unit_height_mm * row_units[row_index]:g}"
)
center_grid_svg = (
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119 26" '
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 119 {header_height_mm:g}" '
'preserveAspectRatio="none">'
'<rect width="119" height="26" fill="white"/>'
'<path d="M80 0V26 M0 6.5H119 M0 13H119 M0 19.5H119" '
f'<rect width="119" height="{header_height_mm:g}" fill="white"/>'
f'<path d="{" ".join(grid_paths)}" '
'fill="none" stroke="#888" stroke-width="0.14"/>'
'</svg>'
)
center_grid_data = base64.b64encode(center_grid_svg.encode("utf-8")).decode("ascii")
center_grid_background = f'url("data:image/svg+xml;base64,{center_grid_data}")'
middle = "\n".join(
[
f"{fields['category']} | {card_prefix(card)}: {card.get('title', '')}",
f"Projekt: {card.get('project', '')} · Seria: {card.get('series_title', card.get('series', ''))}",
f"Przedmiot: {card.get('subject', '')}",
f"Poziom: {card.get('level', '')}",
url,
]
)
meta_before_page = "\n".join(
[
f"DATA {card.get('revision_date', '')} · STATUS {card.get('status', '')}",
f"KARTA {card.get('number', '')}/{card.get('count', '')} · WERSJA {card.get('version', '')}",
"STRONA ",
]
)
meta_after_page = "\n".join(
[
f" · AUTOR {card.get('author', '')}",
f"UUID {card.get('uuid', '')}",
]
)
qr_box = (
f"height:{header_height_mm:g}mm;box-sizing:border-box;padding:24mm .8mm .5mm;"
f"height:{header_height_mm:g}mm;box-sizing:border-box;padding:0;"
"border:1px solid #111;background-color:#fff;background-repeat:no-repeat;"
"background-position:center 1mm;background-size:22mm 22mm;"
"background-position:center;background-size:22mm 22mm;"
"vertical-align:middle;text-align:center;"
"font:700 5px/1 ui-monospace,SFMono-Regular,Consolas,monospace;"
"letter-spacing:.08em;color:#444"
"font:0/0 sans-serif;color:transparent"
)
if repository_qr_enabled:
def header_row(primary: object = "", meta: object = "") -> str:
left = str(primary).replace("\n", " ")[:92]
right = str(meta).replace("\n", " ")
return f"{left:<92} {right}"
center_before_page = "\n".join(
[
header_row(
f"{fields['category']} | {card_prefix(card)}: {card.get('title', '')}",
f"DATA {card.get('revision_date', '')} | STATUS {card.get('status', '')}",
),
header_row(
f"PROJEKT {card.get('project', '')} | SERIA {card.get('series_title', card.get('series', ''))}",
f"KARTA {card.get('number', '')}/{card.get('count', '')} | WERSJA {card.get('version', '')}",
),
header_row(
f"PRZEDMIOT {card.get('subject', '')} | POZIOM {card.get('level', '')}",
f"AUTOR {card.get('author', '')} | ARKUSZ ",
),
def print_row(row: list[dict], total_characters: int) -> list[str]:
if len(row) == 1 and row[0].get("kind") == "urls":
repository = f'GITEA {row[0].get("repository_url", "")}'
html_url = f'HTML {row[0].get("url", "")}'
return [
" " * total_characters,
repository[:total_characters].ljust(total_characters),
html_url[:total_characters].ljust(total_characters),
" " * total_characters,
]
character_widths: list[int] = []
assigned = 0
for field in row[:-1]:
width = max(3, round(float(field["width"]) / 119 * total_characters))
character_widths.append(width)
assigned += width
character_widths.append(total_characters - assigned)
key_cells: list[str] = []
value_cells: list[str] = []
for field, width in zip(row, character_widths):
if field.get("kind") == "identity":
host_uuid = str(field.get("uuid_gitea", ""))
card_uuid = str(field.get("uuid_card", ""))
gitea_identity = f' GITEA UUID {host_uuid}'
card_identity = f' CARD UUID {card_uuid}'
author = f'AUTHOR {field.get("author", "")}'
key_cells.append(gitea_identity[:width].ljust(width))
available = max(1, width - len(author))
value_cells.append(
(card_identity[:available].ljust(available) + author)[:width].ljust(width)
)
center_after_page = "\n" + header_row(
f"HTML {url}",
f"UUID {card.get('uuid', '')}",
)
footer_left = (
f"KARTA {card.get('number', '')}/{card.get('count', '')}\n"
f"WERSJA {card.get('version', '')} · {card.get('revision_date', '')}"
)
footer_center = f"UUID {card.get('uuid', '')}\nGITEA {source_url}"
footer_right_before_page = "ARKUSZ "
continue
label = str(field.get("label", ""))
value = str(field.get("value", "") or "")
key_text = f" {label}" if label else ""
value_text = f" {value}"
if len(key_text) > width:
key_text = key_text[: max(1, width - 1)] + ""
if len(value_text) > width:
value_text = value_text[: max(1, width - 1)] + ""
key_cells.append(key_text.ljust(width))
value_cells.append(value_text.ljust(width))
return ["".join(key_cells), "".join(value_cells)]
print_characters = 139 if repository_qr_enabled else 166
print_lines = [line for row in print_layout for line in print_row(row, print_characters)]
center_text = "\n".join(print_lines)
center_before_page, center_after_page = center_text.split(sheet_token, 1)
print_line_height_mm = (header_height_mm - 0.4) / len(print_lines)
if repository_qr_enabled:
return f"""
@page{{
size:A4;
margin:{page_top_margin_mm:g}mm 21.5mm 15mm;
@top-left{{content:"GITEA";width:24mm;{qr_box};background-image:{repository_qr_background}}}
@top-center{{content:"{css_paged_string(center_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(center_after_page)}";width:119mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:1mm;border-block:1px solid #111;background-color:#fff;background-image:{center_grid_background};background-repeat:no-repeat;background-position:center;background-size:100% 100%;white-space:pre;vertical-align:middle;text-align:left;font:600 5.3px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111;overflow:hidden}}
@top-right{{content:"HTML";width:24mm;{qr_box};background-image:{page_qr_background}}}
@bottom-left{{content:"{css_paged_string(footer_left)}";width:45mm;height:7mm;box-sizing:border-box;padding:.7mm 1mm 0;border-top:1px solid #111;white-space:pre;text-align:left;font:500 4.8px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace}}
@bottom-center{{content:"{css_paged_string(footer_center)}";width:90mm;height:7mm;box-sizing:border-box;padding:.7mm 1mm 0;border-top:1px solid #111;white-space:pre;text-align:left;font:500 4.8px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace;overflow:hidden}}
@bottom-right{{content:"{css_paged_string(footer_right_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(chr(10) + 'AUTOR ' + str(card.get('author', '')))}";width:32mm;height:7mm;box-sizing:border-box;padding:.7mm 1mm 0;border-top:1px solid #111;white-space:pre;text-align:right;font:500 4.8px/1.2 ui-monospace,SFMono-Regular,Consolas,monospace}}
margin:{page_top_margin_mm:g}mm 21.5mm 5mm;
@top-left{{content:"";width:24mm;{qr_box};background-image:{repository_qr_background}}}
@top-center{{content:"{css_paged_string(center_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(center_after_page)}";width:119mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:.2mm .7mm 0;border-block:1px solid #111;background-color:#fff;background-image:{center_grid_background};background-repeat:no-repeat;background-position:center;background-size:100% 100%;white-space:pre;vertical-align:top;text-align:left;font:700 5.2px/{print_line_height_mm:g}mm ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:-.02px;color:#111;overflow:hidden}}
@top-right{{content:"";width:24mm;{qr_box};background-image:{page_qr_background}}}
}}
"""
return f"""
@page{{
size:A4;
margin:{page_top_margin_mm:g}mm 21.5mm 15mm;
@top-left{{content:"{css_paged_string(meta_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(meta_after_page)}";width:47mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:1.5mm 1.7mm;border:1px solid #111;white-space:pre-wrap;vertical-align:middle;text-align:left;font:600 5.7px/1.3 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111}}
@top-center{{content:"{css_paged_string(middle)}";width:96mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:1.5mm 2mm;border-block:1px solid #111;white-space:pre-wrap;vertical-align:middle;text-align:left;font:600 6.2px/1.25 ui-monospace,SFMono-Regular,Consolas,monospace;color:#111}}
@top-right{{content:"HTML";width:24mm;{qr_box};background-image:{page_qr_background}}}
margin:{page_top_margin_mm:g}mm 21.5mm 5mm;
@top-left{{content:"{css_paged_string(center_before_page)}" counter(page) "/" counter(pages) "{css_paged_string(center_after_page)}";width:143mm;height:{header_height_mm:g}mm;box-sizing:border-box;padding:.2mm .7mm 0;border-block:1px solid #111;border-left:1px solid #111;background-color:#fff;background-image:{center_grid_background};background-repeat:no-repeat;background-position:center;background-size:100% 100%;white-space:pre;vertical-align:top;text-align:left;font:700 5.2px/{print_line_height_mm:g}mm ui-monospace,SFMono-Regular,Consolas,monospace;letter-spacing:-.02px;color:#111;overflow:hidden}}
@top-right{{content:"";width:24mm;{qr_box};background-image:{page_qr_background}}}
}}
"""
@@ -3483,75 +3534,129 @@ def render_page_header_tex_macro(data: dict) -> list[str]:
else r"{\tiny QR wyłączony}"
)
source_qr = rf"\qrcode[level=L,height=22mm]{{{source_url}}}" if dual_qr else ""
title = f"{card_prefix(card)}: {card.get('title', '')}"
card_number = f"{card.get('number', '')}/{card.get('count', '')}"
primary_width = "76mm" if dual_qr else "89mm"
meta_width = "39mm" if dual_qr else "47mm"
row_height_mm = (inner_height_mm - 0.5) / 4
layout = resource_header_layout(data, r"\thepage/\pageref{LastPage}")
row_units = [1, 1, 1, 2]
unit_height_mm = (inner_height_mm - (3 * 0.35 * 0.35146)) / sum(row_units)
row_heights_mm = [unit_height_mm * units for units in row_units]
def header_rows_tex(rows: list[str]) -> list[str]:
output: list[str] = []
for index, row in enumerate(rows):
output.append(
" \\parbox[c]["
+ f"{row_height_mm:g}mm"
+ "][c]{\\linewidth}{\\hspace{0.65mm}"
+ row
+ "\\hspace{0.65mm}}%"
def tex_header_cell(field: dict, height_mm: float) -> str:
kind = str(field.get("kind", ""))
if kind == "identity":
uuid_gitea = tex_escape(field.get("uuid_gitea", ""))
uuid_card = tex_escape(field.get("uuid_card", ""))
author = tex_escape(field.get("author", ""))
return (
rf"\parbox[c][{height_mm:g}mm][c]{{\linewidth}}{{\hspace{{0.35mm}}"
rf"{{\fontsize{{3.15}}{{3.55}}\selectfont\ttfamily\bfseries GITEA UUID}} "
rf"{{\fontsize{{3.15}}{{3.55}}\selectfont\ttfamily {uuid_gitea}}}\par\nointerlineskip"
rf"\hspace{{0.35mm}}{{\fontsize{{3.15}}{{3.55}}\selectfont\ttfamily\bfseries CARD UUID}} "
rf"{{\fontsize{{3.15}}{{3.55}}\selectfont\ttfamily {uuid_card}}}"
rf"\hfill{{\fontsize{{3.15}}{{3.55}}\selectfont\ttfamily\bfseries AUTHOR {author}}}\hspace{{0.35mm}}}}"
)
if index != len(rows) - 1:
output.extend(
if kind == "urls":
repository = str(field.get("repository_url", ""))
html_url = str(field.get("url", ""))
return (
rf"\parbox[c][{height_mm:g}mm][c]{{\linewidth}}{{\hspace{{0.45mm}}"
rf"{{\fontsize{{4.15}}{{4.5}}\selectfont\ttfamily\bfseries GITEA}} "
rf"{{\fontsize{{4.15}}{{4.5}}\selectfont\ttfamily\href{{{repository}}}{{\nolinkurl{{{repository}}}}}}}\par\nointerlineskip"
rf"\hspace{{0.45mm}}{{\fontsize{{4.15}}{{4.5}}\selectfont\ttfamily\bfseries HTML}} "
rf"{{\fontsize{{4.15}}{{4.5}}\selectfont\ttfamily\href{{{html_url}}}{{\nolinkurl{{{html_url}}}}}}}}}"
)
label = str(field.get("label", ""))
value = str(field.get("value", "") or "")
value_tex = value if field.get("key") == "sheet" else tex_escape(value)
if field.get("title"):
value_size, value_leading = 6.8, 7.0
elif field.get("emphasis"):
value_size, value_leading = 5.8, 6.0
elif field.get("url"):
value_size, value_leading = 5.3, 5.6
elif field.get("compact"):
value_size, value_leading = 3.9, 4.1
else:
value_size, value_leading = 5.6, 5.8
compact = bool(field.get("compact"))
label_size, label_leading = (2.5, 2.7) if compact else (2.8, 3.0)
left_padding_mm = 0.15 if compact else 0.45
label_tex = (
rf"{{\fontsize{{{label_size:g}}}{{{label_leading:g}}}\selectfont\ttfamily\bfseries {tex_escape(label)}}}"
if label
else ""
)
value_content = (
rf"{{\fontsize{{{value_size:g}}}{{{value_leading:g}}}\selectfont\ttfamily\bfseries {value_tex}}}"
)
href = str(field.get("href", ""))
if href:
value_content = rf"\href{{{href}}}{{{value_content}}}"
return (
rf"\parbox[c][{height_mm:g}mm][c]{{\linewidth}}{{"
rf"\hspace{{{left_padding_mm:g}mm}}{label_tex}\par\nointerlineskip\vspace{{0.12mm}}"
rf"\hspace{{{left_padding_mm:g}mm}}{value_content}\hspace{{0.15mm}}}}"
)
def tex_header_row(row: list[dict], height_mm: float) -> list[str]:
fixed_columns = "|".join(
# The vertical rule belongs to the physical width of each fixed
# segment. Subtract it from the p-column itself so cumulative
# boundaries (especially the 59.5 mm midpoint) stay aligned even
# when a row contains a different number of subcolumns.
f'p{{\\dimexpr{float(field["width"]) / 119:.8f}\\linewidth-\\arrayrulewidth\\relax}}'
for field in row[:-1]
)
column_spec = f"@{{}}{fixed_columns}|X@{{}}" if fixed_columns else "@{}X@{}"
cells = " & ".join(tex_header_cell(field, height_mm) for field in row)
return [
rf" \begin{{tabularx}}{{\linewidth}}{{{column_spec}}}%",
rf" {cells} \\%",
r" \end{tabularx}%",
]
center_rows: list[str] = []
for index, row in enumerate(layout):
center_rows.extend(tex_header_row(row, row_heights_mm[index]))
if index != len(layout) - 1:
center_rows.extend(
[
r" \par\nointerlineskip%",
r" \hrule height0.35pt%",
r" \nointerlineskip%",
]
)
return output
primary_rows = [
rf"{{\fontsize{{5.0}}{{5.4}}\selectfont\ttfamily {tex_escape(fields['category'])} | \textbf{{{tex_escape(title)}}}}}",
rf"{{\fontsize{{4.8}}{{5.2}}\selectfont\ttfamily \textbf{{PROJEKT}} {tex_escape(card.get('project', ''))} | \textbf{{SERIA}} {tex_escape(card.get('series_title', card.get('series', '')))}}}",
rf"{{\fontsize{{4.5}}{{5.0}}\selectfont\ttfamily \textbf{{PRZEDMIOT}} {tex_escape(card.get('subject', ''))} | \textbf{{POZIOM}} {tex_escape(card.get('level', ''))}}}",
rf"{{\fontsize{{3.8}}{{4.2}}\selectfont\ttfamily \textbf{{HTML}} \href{{{url}}}{{\nolinkurl{{{url}}}}}}}",
]
meta_rows = [
rf"{{\fontsize{{4.6}}{{5.0}}\selectfont\ttfamily \textbf{{DATA}} {tex_escape(card.get('revision_date', ''))} | \textbf{{STATUS}} {tex_escape(card.get('status', ''))}}}",
rf"{{\fontsize{{4.6}}{{5.0}}\selectfont\ttfamily \textbf{{KARTA}} {tex_escape(card_number)} | \textbf{{WERSJA}} {tex_escape(card.get('version', ''))}}}",
rf"{{\fontsize{{4.6}}{{5.0}}\selectfont\ttfamily \textbf{{AUTOR}} {tex_escape(card.get('author', ''))} | \textbf{{ARKUSZ}} \thepage/\pageref{{LastPage}}}}",
rf"{{\fontsize{{3.8}}{{4.2}}\selectfont\ttfamily \textbf{{UUID}} {tex_escape(card.get('uuid', ''))}}}",
]
source_cell = (
[
rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{24mm}}\centering%",
" " + source_qr + r"\par%",
r" \vspace{0.2mm}{\fontsize{4.5}{4.8}\selectfont\bfseries\ttfamily GITEA}%",
" " + source_qr + r"%",
r" \end{minipage}%",
r" \vrule width0.35pt\hspace{1mm}%",
r" \vrule width0.35pt%",
]
if dual_qr
else []
)
center_width = (
r"\dimexpr\linewidth-48mm-0.7pt\relax"
if dual_qr
else r"\dimexpr\linewidth-24mm-0.35pt\relax"
)
return [
r"\newcommand{\ESCPageResourceHeader}{%",
r" \begingroup%",
r" \setlength{\parskip}{0pt}%",
r" \setlength{\fboxsep}{0pt}%",
r" \setlength{\fboxrule}{0.35pt}%",
r" \setlength{\arrayrulewidth}{0.35pt}%",
r" \noindent\fbox{%",
rf" \begin{{minipage}}[c][{outer_height_mm:g}mm][c]{{\dimexpr\textwidth-2\fboxrule\relax}}%",
*source_cell,
rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{{primary_width}}}%",
*header_rows_tex(primary_rows),
rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{{center_width}}}%",
r" \setlength{\tabcolsep}{0pt}%",
r" \renewcommand{\arraystretch}{0}%",
*center_rows,
r" \end{minipage}%",
r" \hfill\vrule width0.35pt\hspace{1mm}%",
rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{{meta_width}}}%",
*header_rows_tex(meta_rows),
r" \end{minipage}%",
r" \hfill\vrule width0.35pt\hspace{1mm}%",
r" \vrule width0.35pt%",
rf" \begin{{minipage}}[c][{inner_height_mm:g}mm][c]{{24mm}}\centering%",
" " + page_qr + r"\par%",
r" \vspace{0.2mm}{\fontsize{4.5}{4.8}\selectfont\bfseries\ttfamily HTML}%",
" " + page_qr + r"%",
r" \end{minipage}%",
r" \end{minipage}%",
r" }%",