#!/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()