"""E05 보고서 인쇄 — 목차 · 쪽 번호 · 머리말/꼬리말 · 긴 표 쪽 나눔(요구 347-5 · 351 · PLAN 534-5). fill_toc(sheet, book, pages) 목차 줄(장 · 내용 · 쪽) — 쪽 없으면 「-」 number_toc(book) 시트별 쪽 수를 재서 목차 쪽 번호를 채움(브라우저 한 번) report_pdf(book, figures) 표지(쪽 번호 없음) + 본문(머리말 사업명 · 꼬리말 n / N) 한 벌 balance_breaks(sheet, book, first, last) 긴 표 쪽 나눔 — 쪽마다 고르게(마지막 쪽에 한두 줄만 남지 않게) 쪽 번호 = 표지를 뺀 본문 차례(목차 = 1 쪽). 쪽 수 = 시트 하나씩 인쇄 HTML → PDF 쪽 수 (시트 경계 = 늘 새 쪽 · `spreadsheet_print_html.ts`). 쪽 재기 · PDF = 공용 PDF 엔진(헤드리스). """ from __future__ import annotations import io import math from html import escape from typing import Any from common_util.common_util_print_pdf import merge_pdfs, render_htmls_to_pdfs from common_util.common_util_spreadsheet import print_htmls from E05_StabReport.E05_StabReport_Cells import A1, _set COVER, TOC = "s1", "s9" TOC_FIRST = 4 # 목차 줄 = 4 행부터(4 행 서식) TITLES = { "s2": "개요 및 판정 요약", "s3": "입력 조건 — 해석 방법 · 기준 안전율 · 지하수 · 하중 · 물성치", "s4": "측점별 안정성 판정 일람표", "s5": "대표 단면 해석 결과 — 단면도 · 임계 활동면 · 안전율", "s6": "3D 해석 요약", "s7": "광역 자연사면 위험 요약", "s8": "보강 검토 — 비탈 완화 · 소단 · 벽 외적 안정", "s11": "암반 비탈면 검토 — 평사투영 · 평면 · 쐐기 안전율", "s12": "확률 해석 · 민감도 — 파괴확률 · 신뢰도 지수", "s10": "대표 단면 절편 계산표", } APPENDIX = {"s10"} REPORT_NAME = "비탈면 안정성 검토 보고서" FONT = "font-family:'Malgun Gothic','맑은 고딕',sans-serif" PAPER_MM = {"A4": (210, 297), "A3": (297, 420)} PX_MM = 96 / 25.4 def _row(a1: str) -> int: return int(A1.match(a1.split(":")[0]).group(2)) def _visible(book: dict[str, Any]) -> list[dict[str, Any]]: return [s for s in book["시트"] if not s.get("숨김")] def _start_pages(book: dict[str, Any], pages: dict[str, int]) -> dict[str, int]: """시트 id → 본문 시작 쪽(표지 뺌 · 첫 본문 시트 = 1).""" out, at = {}, 1 for s in _visible(book): if s["id"] == COVER: continue out[s["id"]] = at at += pages.get(s["id"], 0) return out def fill_toc(sheet: dict[str, Any], book: dict[str, Any], pages: dict[str, int] | None) -> None: """목차 — 보이는 본문 시트(표지 · 목차 뺌)마다 한 줄 · 장 번호 · 부록 · 시작 쪽.""" tpl = {c: dict(sheet["칸"].get(f"{c}{TOC_FIRST}") or {}) for c in "ABC"} for a1 in [a for a in sheet["칸"] if _row(a) >= TOC_FIRST]: cell = sheet["칸"][a1] if _row(a1) == TOC_FIRST: # 양식 줄 = 서식만 남김 cell.pop("값", None) else: del sheet["칸"][a1] height = (sheet.get("행") or {}).get(str(TOC_FIRST)) start = _start_pages(book, pages) if pages else {} chapter = 0 for i, s in enumerate(x for x in _visible(book) if x["id"] not in (COVER, TOC)): r = TOC_FIRST + i for c, cell in tpl.items(): sheet["칸"][f"{c}{r}"] = {k: v for k, v in cell.items() if k != "값"} if height: sheet["행"][str(r)] = dict(height) if s["id"] in APPENDIX: mark = "부록" else: chapter += 1 mark = f"{chapter}." _set(sheet, f"A{r}", mark) _set(sheet, f"B{r}", TITLES.get(s["id"], s["이름"])) _set(sheet, f"C{r}", start.get(s["id"], "-")) def _only(book: dict[str, Any], keep: set[str], hide: bool = False) -> dict[str, Any]: """`keep` 시트만 보이는(hide = 참이면 `keep` 만 숨긴) 얕은 사본 · 브라우저 계산값 뺌.""" sheets = [{**s, "숨김": True} if (s["id"] in keep) == hide else s for s in book["시트"]] return {**{k: v for k, v in book.items() if k != "계산값"}, "시트": sheets} def _figures(book: dict[str, Any], ids: set[str], figures: dict[str, bytes]) -> dict[str, bytes]: keys = {f["열쇠"] for s in book["시트"] if s["id"] in ids for f in s.get("그림") or []} return {k: v for k, v in figures.items() if k in keys} def sheet_pages(book: dict[str, Any]) -> dict[str, int]: """보이는 시트마다 인쇄 쪽 수(그림 없이 — 그림 칸 크기는 행열이 정함).""" from pypdf import PdfReader ids = [s["id"] for s in _visible(book)] htmls = print_htmls([{"book": _only(book, {sid})} for sid in ids]) pdfs = render_htmls_to_pdfs([{"html": h, "page_size": None} for h in htmls]) return {sid: len(PdfReader(io.BytesIO(pdf)).pages) for sid, pdf in zip(ids, pdfs)} def number_toc(book: dict[str, Any]) -> dict[str, int]: """목차 시트가 있으면 쪽 수를 재서 쪽 번호를 채움(책을 고침) · 시트별 쪽 수.""" pages = sheet_pages(book) toc = next((s for s in book["시트"] if s["id"] == TOC and not s.get("숨김")), None) if toc is not None: fill_toc(toc, book, pages) return pages def _header(project: str) -> str: return ( f'