diff --git a/scripts/check_repository.py b/scripts/check_repository.py
index 858dc27..5e82c0b 100644
--- a/scripts/check_repository.py
+++ b/scripts/check_repository.py
@@ -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}")
diff --git a/tools/render_card.py b/tools/render_card.py
index 6c2ec42..cb8779e 100644
--- a/tools/render_card.py
+++ b/tools/render_card.py
@@ -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''
- f'{qr}{html_escape(label)}'
+ f'{qr}'
)
return (
f'
'
@@ -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('
|')
- rendered.append(f'
{html_escape(label)}')
- tag = "strong" if strong_first and index == 0 else "span"
- rendered.append(f'<{tag} class="resource-value">{html_escape(value or "—")}{tag}>')
- return '
' + "".join(rendered) + "
"
+ def render_field(field: dict) -> str:
+ kind = str(field.get("kind", ""))
+ if kind == "identity":
+ return (
+ ''
+ )
+ if kind == "urls":
+ repository_target = str(field.get("repository_url", ""))
+ html_target = str(field.get("url", ""))
+ return (
+ ''
+ )
+ 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'
{html_escape(label)}' if label else ""
+ value_html = f'
{html_escape(value)}'
+ content = label_html + value_html
+ href = str(field.get("href", ""))
+ if href:
+ content = (
+ f'
'
+ f'{content}'
+ )
+ return f'
{content}
'
- 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''
+ )
+
+ center_rows = "".join(render_row(row) for row in layout)
column_group = (
- '
'
+ '
'
if repository_qr_html
- else '
'
+ else '
'
+ )
+ left_cell = (
+ f'
{repository_qr_html} | '
+ 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'
{repository_qr_html} | ')
- cells.append(f'')
- cells.append(f'')
- if index == 0:
- cells.append(f'
{page_qr_html} | ')
- rows.append("
" + "".join(cells) + "
")
return (
f''
)
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 (
- ''
- )
+ """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 = (
- '
'
)
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" }%",