feat: refine repeated dual-QR header
This commit is contained in:
@@ -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}")
|
||||
|
||||
+313
-208
@@ -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>"
|
||||
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 {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> {html_escape(repository_target)}</a>'
|
||||
f'<a href="{html_escape(html_target)}"><b>HTML</b> {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>'
|
||||
|
||||
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", ""))]),
|
||||
]
|
||||
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:39mm"><col style="width:24mm">'
|
||||
'<col style="width:24mm"><col><col style="width:24mm">'
|
||||
if repository_qr_html
|
||||
else '<col><col style="width:47mm"><col style="width:24mm">'
|
||||
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,
|
||||
]
|
||||
)
|
||||
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 "
|
||||
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)
|
||||
)
|
||||
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(
|
||||
[
|
||||
r" \par\nointerlineskip%",
|
||||
r" \hrule height0.35pt%",
|
||||
r" \nointerlineskip%",
|
||||
]
|
||||
)
|
||||
return output
|
||||
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}}}}"
|
||||
)
|
||||
|
||||
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', ''))}}}",
|
||||
]
|
||||
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%",
|
||||
]
|
||||
)
|
||||
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" }%",
|
||||
|
||||
Reference in New Issue
Block a user