222 lines
6.7 KiB
Python
222 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import datetime as dt
|
||
import os
|
||
import subprocess
|
||
import tempfile
|
||
from pathlib import Path
|
||
from typing import Iterable
|
||
|
||
from openpyxl import load_workbook
|
||
|
||
|
||
def _excel_col_letter(index_1_based: int) -> str:
|
||
result = ""
|
||
n = index_1_based
|
||
while n:
|
||
n, rem = divmod(n - 1, 26)
|
||
result = chr(ord("A") + rem) + result
|
||
return result
|
||
|
||
|
||
def _stringify_cell(value) -> str:
|
||
if value is None:
|
||
return ""
|
||
if isinstance(value, (dt.datetime, dt.date)):
|
||
return value.isoformat()
|
||
return str(value)
|
||
|
||
|
||
def _troff_escape(text: str) -> str:
|
||
text = text.replace("\\", "\\\\")
|
||
text = text.replace("\t", " ")
|
||
text = text.replace("|", "¦")
|
||
return text
|
||
|
||
|
||
def _troff_field(text: str) -> str:
|
||
text = _troff_escape(text)
|
||
if not text:
|
||
return "T{\n\\&\nT}"
|
||
lines = text.splitlines() or [text]
|
||
safe_lines: list[str] = []
|
||
for line in lines:
|
||
if line.startswith((".", "'")):
|
||
safe_lines.append(r"\&" + line)
|
||
else:
|
||
safe_lines.append(line)
|
||
body = "\n".join(safe_lines)
|
||
return f"T{{\n{body}\nT}}"
|
||
|
||
|
||
def _used_bounds(ws) -> tuple[int, int]:
|
||
last_row = 0
|
||
last_col = 0
|
||
for row_idx, row in enumerate(ws.iter_rows(values_only=True), start=1):
|
||
if any(v not in (None, "") for v in row):
|
||
last_row = row_idx
|
||
for col_idx, v in enumerate(row, start=1):
|
||
if v not in (None, ""):
|
||
last_col = max(last_col, col_idx)
|
||
return last_row, last_col
|
||
|
||
|
||
def _iter_rows(ws, max_row: int, max_col: int) -> Iterable[list[str]]:
|
||
for row in ws.iter_rows(min_row=1, max_row=max_row, min_col=1, max_col=max_col, values_only=True):
|
||
yield [_stringify_cell(v) for v in row]
|
||
|
||
|
||
def _emit_table(rows: list[list[str]], *, col_slice: slice, title: str) -> str:
|
||
cols = list(range(col_slice.start or 0, col_slice.stop or len(rows[0])))
|
||
ncols = len(cols)
|
||
if ncols <= 0:
|
||
return ""
|
||
|
||
spec = " ".join(["l"] * ncols) + "."
|
||
out: list[str] = []
|
||
out.append(".LP")
|
||
out.append(rf"\fB{_troff_escape(title)}\fP")
|
||
out.append(".TS")
|
||
out.append("tab(|) expand;")
|
||
out.append(spec)
|
||
|
||
header = rows[0]
|
||
header_fields = []
|
||
for i in cols:
|
||
header_fields.append(_troff_field(header[i]))
|
||
out.append("|".join(header_fields))
|
||
|
||
for row in rows[1:]:
|
||
fields = [_troff_field(row[i]) for i in cols]
|
||
out.append("|".join(fields))
|
||
|
||
out.append(".TE")
|
||
out.append(".LP")
|
||
return "\n".join(out) + "\n"
|
||
|
||
|
||
def build_troff_from_xlsx(xlsx_path: Path, generated_at: dt.datetime) -> str:
|
||
wb = load_workbook(xlsx_path, data_only=False)
|
||
|
||
lines: list[str] = []
|
||
lines.append('.\" Auto-generated from XLSX')
|
||
lines.append(".po 1i")
|
||
lines.append(".ll 6.5i")
|
||
lines.append(".ps 10")
|
||
lines.append(".vs 12")
|
||
lines.append("")
|
||
|
||
lines.append(".ps 18")
|
||
lines.append(".vs 22")
|
||
lines.append(".ce")
|
||
lines.append(r"\fBDefinição de Ranking — AtuaCAPES\fP")
|
||
lines.append(".sp 0.5")
|
||
lines.append(".ps 11")
|
||
lines.append(".vs 14")
|
||
lines.append(".ce")
|
||
lines.append("Documento gerado automaticamente")
|
||
lines.append(".sp 0.5")
|
||
lines.append(".ce")
|
||
lines.append(_troff_escape(generated_at.strftime("%Y-%m-%d %H:%M")))
|
||
lines.append(".sp 1")
|
||
lines.append(rf"Fonte: \fB{_troff_escape(str(xlsx_path))}\fP")
|
||
lines.append(".LP")
|
||
lines.append("Conteúdo: todas as abas da planilha, com critérios e regras conforme definidos no arquivo de entrada.")
|
||
lines.append(".bp")
|
||
|
||
lines.append(".ps 14")
|
||
lines.append(".vs 18")
|
||
lines.append(r"\fBSumário de Abas\fP")
|
||
lines.append(".ps 10")
|
||
lines.append(".vs 12")
|
||
lines.append(".sp 0.5")
|
||
for i, name in enumerate(wb.sheetnames, start=1):
|
||
lines.append(rf"{i}. \fB{_troff_escape(name)}\fP")
|
||
lines.append(".br")
|
||
lines.append(".bp")
|
||
|
||
for sheet_name in wb.sheetnames:
|
||
ws = wb[sheet_name]
|
||
max_row, max_col = _used_bounds(ws)
|
||
if max_row == 0 or max_col == 0:
|
||
continue
|
||
|
||
rows = list(_iter_rows(ws, max_row=max_row, max_col=max_col))
|
||
if not rows:
|
||
continue
|
||
|
||
lines.append(".ps 14")
|
||
lines.append(".vs 18")
|
||
lines.append(rf"\fB{_troff_escape(sheet_name)}\fP")
|
||
lines.append(".ps 10")
|
||
lines.append(".vs 12")
|
||
lines.append(".LP")
|
||
lines.append(rf"Linhas: \fB{max_row}\fP — Colunas: \fB{max_col}\fP")
|
||
|
||
max_cols_per_table = 8
|
||
if max_col <= max_cols_per_table:
|
||
start_letter = _excel_col_letter(1)
|
||
end_letter = _excel_col_letter(max_col)
|
||
lines.append(_emit_table(rows, col_slice=slice(0, max_col), title=f"Colunas {start_letter}–{end_letter}"))
|
||
else:
|
||
start = 0
|
||
while start < max_col:
|
||
end = min(start + max_cols_per_table, max_col)
|
||
start_letter = _excel_col_letter(start + 1)
|
||
end_letter = _excel_col_letter(end)
|
||
lines.append(
|
||
_emit_table(
|
||
rows,
|
||
col_slice=slice(start, end),
|
||
title=f"Colunas {start_letter}–{end_letter}",
|
||
)
|
||
)
|
||
start = end
|
||
|
||
lines.append(".bp")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
def render_pdf_from_ms(ms_text: str, pdf_path: Path, *, workdir: Path) -> None:
|
||
pdf_path.parent.mkdir(parents=True, exist_ok=True)
|
||
with tempfile.TemporaryDirectory(dir=workdir) as td:
|
||
td_path = Path(td)
|
||
ms_path = td_path / "doc.ms"
|
||
ps_path = td_path / "doc.ps"
|
||
ms_path.write_text(ms_text, encoding="utf-8")
|
||
|
||
groff_cmd = ["groff", "-Kutf8", "-Tps", "-t", str(ms_path)]
|
||
ps_bytes = subprocess.check_output(groff_cmd)
|
||
ps_path.write_bytes(ps_bytes)
|
||
|
||
subprocess.check_call(["ps2pdf", str(ps_path), str(pdf_path)])
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="Gera PDF (profissional) a partir de planilha XLSX.")
|
||
parser.add_argument("xlsx", type=Path, help="Caminho do XLSX de entrada")
|
||
parser.add_argument(
|
||
"-o",
|
||
"--out",
|
||
type=Path,
|
||
default=Path("out/definicao_ranking_atuacapes.pdf"),
|
||
help="Caminho do PDF de saída (default: out/definicao_ranking_atuacapes.pdf)",
|
||
)
|
||
args = parser.parse_args()
|
||
|
||
xlsx_path: Path = args.xlsx
|
||
out_pdf: Path = args.out
|
||
|
||
generated_at = dt.datetime.now().astimezone()
|
||
ms_text = build_troff_from_xlsx(xlsx_path=xlsx_path, generated_at=generated_at)
|
||
render_pdf_from_ms(ms_text=ms_text, pdf_path=out_pdf, workdir=Path(os.getcwd()))
|
||
print(out_pdf)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|