foxygit / RPA-Remisser Log in
commits tags

/scripts/merge_and_print.py · 8.12 KB

raw
#!/usr/bin/env python
"""Slår ihop flera filer (PDF:er och/eller bilder) till en PDF - och skriver ut den.

Tänkt att köras som ett "Kör Python-script"-steg (run_script) efter att ett
eller flera "Ladda ner bilaga" (save_attachment) / "Fånga PDF" (capture_pdf)
-steg har hämtat hem filerna, men fungerar lika bra fristående.

Filordning = den ordning filerna anges. Bilder (.jpg/.png/...) läggs som en
centrerad, skalad A4-sida; PDF:er läggs in sida för sida som de är.

Exempel:
    # Explicita filer i vald ordning -> merged.pdf, skriv INTE ut
    python scripts/merge_and_print.py --file data/downloads/remiss.pdf --file data/downloads/karta.jpg

    # Läs fillistan från en JSON-array (t.ex. sparad med "Spara variabel")
    python scripts/merge_and_print.py --files-from bilagor.json --out data/downloads/klar.pdf --print

    # Nyaste PDF:en i data/downloads/ + en bild
    python scripts/merge_and_print.py --pdf-latest --image bild.png

I ett flöde (bilagor kan vara 0, 1 eller flera):
    1. click        -> öppna ärendet
    2. list         -> value "etj-file-attachment-link", var "bilaga_element"
    3. loop over "bilaga_element":
         save_attachment -> by css, scope item, value "a[href]", var "bilagor"
    4. save_var     -> var "bilagor", filename "bilagor.json"
    5. run_script   -> path "scripts/merge_and_print.py"
                       args "--files-from bilagor.json --out data/downloads/klar.pdf --print"

Skriver en rad JSON till stdout så run_script kan spara resultatet i en variabel:
    {"out": "...\\klar.pdf", "pages": 3, "printed": true, "skipped": []}

Kräver: pypdf, Pillow  (se requirements.txt)
Utskrift sker via Windows standardhanterare för PDF (os.startfile(..., "print")).
"""

from __future__ import annotations

import argparse
import json
import os
import sys
import tempfile
import time
from pathlib import Path

from PIL import Image
from pypdf import PdfReader, PdfWriter

ROOT_DIR = Path(__file__).resolve().parent.parent
DOWNLOAD_DIR = ROOT_DIR / "data" / "downloads"
EXPORT_DIR = ROOT_DIR / "data" / "exports"

# A4 i punkter (1 pt = 1/72 tum) - PDF:ens standardenhet.
A4_W, A4_H = 595.276, 841.890
IMAGE_RENDER_DPI = 150  # upplösning bilden rastreras till på A4-sidan

IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff", ".webp"}


def _resolve(path_str: str) -> Path:
    """Tillåter absoluta sökvägar och sökvägar relativt projektroten."""
    p = Path(path_str)
    return p if p.is_absolute() else (ROOT_DIR / p)


def _latest_pdf(folder: Path) -> Path:
    pdfs = sorted(folder.glob("*.pdf"), key=lambda p: p.stat().st_mtime, reverse=True)
    if not pdfs:
        sys.exit(f"Hittar ingen *.pdf i {folder}")
    return pdfs[0]


def _load_files_from(spec: str) -> list[str]:
    """Läser en JSON-array med sökvägar. spec = fil i data/exports/ eller sökväg."""
    candidates = [_resolve(spec), EXPORT_DIR / spec]
    for path in candidates:
        if path.exists():
            data = json.loads(path.read_text(encoding="utf-8"))
            if not isinstance(data, list):
                sys.exit(f"{path} innehåller ingen JSON-lista.")
            return [str(x) for x in data]
    sys.exit(f"Hittar ingen fillista: prövade {', '.join(str(c) for c in candidates)}")


def _image_to_pdf_page(image_path: Path, out_path: Path) -> None:
    """Lägger bilden centrerad och skalad på en stående A4-sida, sparad som 1-sidig PDF."""
    img = Image.open(image_path)
    if img.mode not in ("RGB", "L"):
        img = img.convert("RGB")

    page_px = (round(A4_W / 72 * IMAGE_RENDER_DPI), round(A4_H / 72 * IMAGE_RENDER_DPI))
    canvas = Image.new("RGB", page_px, "white")

    scale = min(page_px[0] / img.width, page_px[1] / img.height)
    new_size = (max(1, round(img.width * scale)), max(1, round(img.height * scale)))
    canvas.paste(img.resize(new_size, Image.LANCZOS), ((page_px[0] - new_size[0]) // 2, (page_px[1] - new_size[1]) // 2))
    canvas.save(out_path, "PDF", resolution=float(IMAGE_RENDER_DPI))


def _send_to_printer(pdf_path: Path, printer: str | None) -> None:
    """Skriver ut via Windows standardhanterare. printer=None => standardskrivaren."""
    if os.name != "nt":
        sys.exit("Utskrift stöds bara på Windows i det här scriptet.")
    if printer:
        import ctypes

        rc = ctypes.windll.shell32.ShellExecuteW(
            None, "printto", str(pdf_path), f'"{printer}"', str(pdf_path.parent), 0
        )
        if rc <= 32:
            sys.exit(f"ShellExecute 'printto' misslyckades (kod {rc}). Prova utan --printer.")
    else:
        os.startfile(str(pdf_path), "print")  # noqa: S606  (Windows-only, avsiktligt)
    time.sleep(3)  # ge hanteraren en stund att spoola


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("--file", action="append", default=[], help="Fil att lägga till (PDF eller bild). Upprepa för fler; ordningen bevaras.")
    parser.add_argument("--files-from", help="JSON-array med sökvägar (fil i data/exports/ eller en sökväg)")
    parser.add_argument("--pdf", help="(bakåtkompat) en PDF att lägga först")
    parser.add_argument("--pdf-latest", action="store_true", help="(bakåtkompat) nyaste *.pdf i data/downloads/ först")
    parser.add_argument("--image", action="append", default=[], help="(bakåtkompat) bild att lägga efter PDF:en")
    parser.add_argument("--image-first", action="store_true", help="(bakåtkompat) lägg --image före --pdf")
    parser.add_argument("--out", default=str(DOWNLOAD_DIR / "merged.pdf"), help="Utfil (default data/downloads/merged.pdf)")
    parser.add_argument("--print", dest="do_print", action="store_true", help="Skicka utfilen till skrivare")
    parser.add_argument("--printer", default=None, help="Skrivarnamn (default: Windows standardskrivare)")
    args = parser.parse_args()

    # Bygg en ordnad lista av källfiler.
    ordered: list[str] = []
    if args.pdf_latest:
        ordered.append(str(_latest_pdf(DOWNLOAD_DIR)))
    elif args.pdf:
        ordered.append(args.pdf)
    if args.image_first:
        ordered = args.image + ordered
    else:
        ordered += args.image
    if args.files_from:
        ordered += _load_files_from(args.files_from)
    ordered += args.file

    sources = [_resolve(p) for p in ordered]
    missing = [str(p) for p in sources if not p.exists()]
    if missing:
        sys.exit("Följande filer finns inte:\n  " + "\n  ".join(missing))
    if not sources:
        sys.exit("Inga källfiler angivna (--file / --files-from / --pdf / --image).")

    out_path = _resolve(args.out)
    out_path.parent.mkdir(parents=True, exist_ok=True)

    writer = PdfWriter()
    tmp_files: list[Path] = []
    skipped: list[str] = []

    try:
        for src in sources:
            suffix = src.suffix.lower()
            if suffix == ".pdf":
                try:
                    for page in PdfReader(str(src)).pages:
                        writer.add_page(page)
                except Exception as exc:  # noqa: BLE001
                    skipped.append(f"{src.name}: {exc}")
                continue
            # Bild (eller okänd typ - försök som bild).
            try:
                fd, tmp_name = tempfile.mkstemp(suffix=".pdf", prefix="img_")
                os.close(fd)
                tmp = Path(tmp_name)
                tmp_files.append(tmp)
                _image_to_pdf_page(src, tmp)
                for page in PdfReader(str(tmp)).pages:
                    writer.add_page(page)
            except Exception as exc:  # noqa: BLE001
                skipped.append(f"{src.name}: {exc}")

        if len(writer.pages) == 0:
            sys.exit("Ingen sida kunde läggas till - inget att skriva ut.\n  " + "\n  ".join(skipped))

        with open(out_path, "wb") as f:
            writer.write(f)
    finally:
        for tmp in tmp_files:
            tmp.unlink(missing_ok=True)

    printed = False
    if args.do_print:
        _send_to_printer(out_path, args.printer)
        printed = True

    print(json.dumps(
        {"out": str(out_path), "pages": len(writer.pages), "printed": printed, "skipped": skipped},
        ensure_ascii=False,
    ))


if __name__ == "__main__":
    main()