Files
copydev-imposing-tool/main.py
Jules 2fcc92dbd9 V3.3.0 — Edition contenu, actions CLI, menu contextuel enrichi
- Edition de contenu PDF (Annotations > Editer le contenu) :
  - Onglet Texte : liste le texte detecte, masquer des lignes
  - Onglet Images : liste les images, remplacer ou masquer
- Actions en ligne de commande (--action booklet_a4|booklet_a3|nup_2x2|unbooklet)
- Menu contextuel Windows enrichi :
  - Ouvrir, Livret A4, Livret A3, 2x2 par feuille, Delivretiser
- Bouton Editer dans la barre d'outils droite

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 16:33:43 +01:00

275 lines
8.7 KiB
Python

#!/usr/bin/env python3
"""Copydev Imposing Tool — Point d'entree."""
import sys
import os
import json
import time
import urllib.request
import urllib.error
import urllib.parse
# Support PyInstaller (chemin des resources)
if getattr(sys, 'frozen', False):
BASE_DIR = os.path.dirname(sys.executable)
BUNDLE_DIR = sys._MEIPASS
else:
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
BUNDLE_DIR = BASE_DIR
sys.path.insert(0, BUNDLE_DIR)
from PyQt6.QtWidgets import QApplication, QMessageBox
import hashlib
import platform
import socket
import uuid
LICENSE_API = "https://vision.copydev.fr/api/license.php"
LICENSE_FILE = os.path.join(BASE_DIR, ".license")
def _get_machine_id():
"""Generer un identifiant unique pour cette machine."""
raw = ""
try:
raw += platform.node()
raw += str(uuid.getnode()) # MAC address
except Exception:
raw += "unknown"
return hashlib.sha256(raw.encode()).hexdigest()[:32]
def _get_hostname():
"""Recuperer le nom NetBIOS / hostname."""
try:
return platform.node() or socket.gethostname()
except Exception:
return "inconnu"
def _load_license_cache():
"""Charger le cache de licence local."""
if not os.path.exists(LICENSE_FILE):
return None
try:
with open(LICENSE_FILE, "r") as f:
return json.load(f)
except Exception:
return None
def _save_license_cache(data):
"""Sauvegarder le cache de licence local."""
try:
with open(LICENSE_FILE, "w") as f:
json.dump(data, f)
except Exception:
pass
def _check_license_online(key):
"""Verifier la licence aupres du serveur Vision."""
try:
mid = _get_machine_id()
hname = _get_hostname()
url = f"{LICENSE_API}?key={urllib.parse.quote(key)}&machine_id={urllib.parse.quote(mid)}&hostname={urllib.parse.quote(hname)}"
req = urllib.request.Request(url, headers={"User-Agent": "CopydevImposingTool/1.0"})
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read().decode())
if data.get("valid"):
cache = {
"key": key,
"client": data.get("client", ""),
"product": data.get("product", ""),
"offline_timeout_hours": data.get("offline_timeout_hours", 72),
"last_validated": time.time(),
}
_save_license_cache(cache)
return True, data.get("client", "")
return False, "Licence invalide"
except urllib.error.HTTPError as e:
try:
body = json.loads(e.read().decode())
return False, body.get("error", "Erreur serveur")
except Exception:
return False, f"Erreur HTTP {e.code}"
except Exception as e:
return None, str(e) # None = hors-ligne
def _check_license_background(key):
"""Verification en arriere-plan (thread)."""
import threading
def _bg():
_check_license_online(key)
t = threading.Thread(target=_bg, daemon=True)
t.start()
def _check_license():
"""Verifier la licence au lancement."""
cache = _load_license_cache()
# Si on a un cache valide, lancer direct et verifier en arriere-plan
if cache and cache.get("key"):
key = cache["key"]
timeout_hours = cache.get("offline_timeout_hours", 72)
last_validated = cache.get("last_validated", 0)
elapsed_hours = (time.time() - last_validated) / 3600
if elapsed_hours < timeout_hours:
# Cache encore valide — lancer l'app et verifier en background
_check_license_background(key)
return True
# Cache expire — on doit verifier en ligne
result, msg = _check_license_online(key)
if result is True:
return True
if result is False:
from app.license_dialog import LicenseMessageDialog
LicenseMessageDialog("Licence invalide",
f"Votre licence a ete refusee :\n{msg}\n\n"
"Contactez CopyDev : contact@copydev.fr", "error").exec()
return False
# Hors-ligne et cache expire
from app.license_dialog import LicenseMessageDialog
LicenseMessageDialog("Licence expiree hors-ligne",
f"Impossible de verifier votre licence depuis {int(elapsed_hours)}h.\n"
f"Le delai hors-ligne autorise est de {timeout_hours}h.\n\n"
"Connectez-vous a internet ou contactez CopyDev :\n"
"contact@copydev.fr", "error").exec()
return False
# Pas de cache — demander la cle de licence
from app.license_dialog import LicenseDialog, LicenseMessageDialog
dlg = LicenseDialog()
if dlg.exec() != LicenseDialog.DialogCode.Accepted:
return False
key = dlg.get_key()
if not key:
return False
result, msg = _check_license_online(key)
if result is True:
LicenseMessageDialog("Licence activee",
f"Bienvenue, {msg} !\nVotre licence est activee.", "success").exec()
return True
if result is False:
LicenseMessageDialog("Licence invalide",
f"{msg}\n\nContactez CopyDev : contact@copydev.fr", "error").exec()
return False
# Hors-ligne a la premiere activation
LicenseMessageDialog("Connexion requise",
"Impossible de contacter le serveur de licences.\n"
"Une connexion internet est necessaire pour la premiere activation.\n\n"
"Contactez CopyDev : contact@copydev.fr", "error").exec()
return False
def main():
app = QApplication(sys.argv)
app.setApplicationName("Copydev Imposing Tool")
app.setOrganizationName("Copydev")
# Icone application
from PyQt6.QtGui import QIcon
icon_path = os.path.join(BUNDLE_DIR, "resources", "logo.png")
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
# Splash screen
from app.splash import SplashScreen
splash = SplashScreen()
splash.show()
QApplication.processEvents()
splash.set_status("Verification de la licence...")
QApplication.processEvents()
splash.hide()
if not _check_license():
sys.exit(1)
splash.show()
QApplication.processEvents()
# Plus de verification de MAJ au demarrage — c'est dans Aide > Rechercher une MAJ
splash.set_status("Chargement de l'interface...")
from app.main_window import MainWindow
window = MainWindow()
# Gestion des arguments
args = sys.argv[1:]
action = None
files_to_open = []
i = 0
while i < len(args):
if args[i] == "--action" and i + 1 < len(args):
action = args[i + 1]
i += 2
elif args[i].lower().endswith('.pdf') and os.path.exists(args[i]):
files_to_open.append(args[i])
i += 1
else:
i += 1
if len(files_to_open) == 1:
window._open_pdf(files_to_open[0])
elif len(files_to_open) > 1:
from pypdf import PdfWriter
import tempfile as tf
writer = PdfWriter()
for f in files_to_open:
writer.append(f)
fd, merged = tf.mkstemp(suffix=".pdf")
os.close(fd)
with open(merged, "wb") as out:
writer.write(out)
window._open_pdf(merged)
window._status_label.setText(f"{len(files_to_open)} fichiers fusionnes")
# Actions en ligne de commande
if action and files_to_open:
from PyQt6.QtCore import QTimer
def run_cli_action():
from app.engine.booklet import create_booklet
from app.engine.nup import nup_pages
from app.engine.unbooklet import unbooklet
import tempfile as tf
fd, output = tf.mkstemp(suffix=".pdf")
os.close(fd)
src = files_to_open[0]
try:
if action == "booklet_a4":
create_booklet(src, output, binding="saddle_stitch", sheet_size=(841.89, 595.28))
elif action == "booklet_a3":
create_booklet(src, output, binding="saddle_stitch", sheet_size=(1190.55, 841.89))
elif action == "nup_2x2":
nup_pages(src, output, cols=2, rows=2, sheet_size=(841.89, 595.28))
elif action == "unbooklet":
unbooklet(src, output)
else:
return
window._show_result(output)
window._status_label.setText(f"Action '{action}' executee")
except Exception as e:
QMessageBox.critical(window, "Erreur", str(e))
QTimer.singleShot(500, run_cli_action)
window.show()
splash.close()
sys.exit(app.exec())
if __name__ == "__main__":
main()