diff --git a/bin/setenv.bat b/bin/setenv.bat index 9c1b715..c928cf5 100644 --- a/bin/setenv.bat +++ b/bin/setenv.bat @@ -1,6 +1,6 @@ +chcp 1252 > nul @echo off -REM falls Umlaute in den Pfaden sind: -chcp 65001 > nul + REM ~dp0 steht für das Verzeichnis, in der diese Datei liegt pushd %~dp0\.. @@ -12,15 +12,11 @@ set PROJECT_LIB=%PROJECT%\lib set PROJECT_DATA=%PROJECT%\data set PROJECT_WORK=%PROJECT%\work set PROJECT_TEST=%PROJECT%\testdata +set PROJECT_LOG=%PROJECT%\log -set SIVAS_TEILESTAMM=\\195.243.223.3\sivas\jit\programme\KSbExcelExportSivasTeilestamm.exe -set SIVAS_EXCEL_EXPORT_DIR=%PROJECT_DATA% - -if not exist %PROJECT%\work mkdir %PROJECT%\work if not exist %PROJECT%\data mkdir %PROJECT%\data +if not exist %PROJECT%\log mkdir %PROJECT%\log -set PROJECT_IO_RESULTS=%PROJECT_WORK% -set PROJECT_BOM_RESULTS=%PROJECT_WORK% set INSTALL_DIR="%ONEDRIVE%\Desktop\Kabeltool" set PATH=%PROJECT_BIN%;%PATH% diff --git a/cfg/allgemein.cfg b/cfg/allgemein.cfg index 4e4784e..5ff8eea 100644 --- a/cfg/allgemein.cfg +++ b/cfg/allgemein.cfg @@ -7,3 +7,23 @@ libfile = ils_lib.dxf [Omniflo] libfile = omniflo_lib.dxf +[dxf2lib] +# Text-Positionierung und Formatierung +# Vertikaler Abstand des Texts über dem Block (in DXF-Einheiten) +text_offset_y = 175 +# Horizontaler Abstand des Texts links vom Block (in DXF-Einheiten) +text_offset_x = -150 +# Maximale Zeichenlänge pro Textzeile (bei Überschreitung wird umgebrochen) +max_len = 9 +# Schriftgröße des Texts (in DXF-Einheiten) +text_height = 20 +# Abstand zwischen zwei Textzeilen bei Umbrüchen (in DXF-Einheiten) +line_spacing = 22 + +# Block-Anordnung im Raster +# Horizontaler Abstand zwischen Blöcken in einer Reihe (in DXF-Einheiten) +block_spacing_x = 350 +# Anzahl Blöcke pro Zeile im Raster +blocks_per_row = 20 +# Vertikaler Abstand zwischen Zeilen im Raster (in DXF-Einheiten) +block_spacing_y = 500 diff --git a/lib/dxf2lib.py b/lib/dxf2lib.py index 61e06b6..4927eaf 100644 --- a/lib/dxf2lib.py +++ b/lib/dxf2lib.py @@ -4,15 +4,26 @@ from ezdxf.math import Vec2 import argparse import sys import shutil +import configparser +from utils import check_environment_var, setup_logger +from pathlib import Path -def check_environment_var(env_str: str, fallback: str) -> str: - out_path = os.environ.get(env_str) - if out_path: - return out_path - print(f"[WARN] Umgebungsvariable {env_str} ist nicht gesetzt oder leer. Verwende '{fallback}'.") - return fallback def get_bbox(entities): + """ + Berechnet die Bounding Box für eine Liste von DXF-Entities. + + Args: + entities: Liste von DXF-Entities (ezdxf entities) + + Returns: + Vec2 or None: Zentrum der Bounding Box als Vec2-Objekt oder None, + falls keine gültige Geometrie gefunden wurde + + Note: + Unterstützt POLYLINE, LWPOLYLINE und andere Entity-Typen. + Fehlerhafte Entities werden übersprungen und protokolliert. + """ min_x, min_y = float('inf'), float('inf') max_x, max_y = float('-inf'), float('-inf') for e in entities: @@ -43,6 +54,166 @@ def get_bbox(entities): return Vec2((min_x + max_x) / 2, (min_y + max_y) / 2) + +def create_block_library(input_dir, output_file, config, logger=None): + """ + Erstellt eine DXF-Block-Bibliothek aus einzelnen DXF-Dateien. + + Diese Funktion liest alle DXF-Dateien aus einem Verzeichnis und erstellt + daraus eine Bibliothek mit Blöcken. Die Blöcke werden in einem Raster + angeordnet und mit Beschriftungen versehen. Fehlerhafte Dateien werden + protokolliert. + + Args: + input_dir (str): Verzeichnis mit den zu verarbeitenden DXF-Dateien + output_file (str): Pfad zur zu erstellenden Bibliotheks-DXF-Datei + config: ConfigParser-Objekt mit den Konfigurationswerten + logger: Optionaler Logger für Logging-Ausgaben + + Note: + - Unterstützte Entity-Typen: LINE, LWPOLYLINE, POLYLINE, SPLINE, CIRCLE, ARC + - Blöcke werden zentriert und in einem 20x20 Raster angeordnet + - Erstellt eine Log-Datei mit Zeitstempel für fehlerhafte Dateien + - Automatische Erstellung des Output-Verzeichnisses falls nötig + """ + doc = ezdxf.new() + msp = doc.modelspace() + + x_offset = 0 + y_offset = 0 + blocks_in_row = 0 + + error_files = [] + processed_files = [] + + for filename in os.listdir(input_dir): + if not filename.lower().endswith(".dxf"): + continue + + filepath = os.path.join(input_dir, filename) + name = os.path.splitext(filename)[0] + + try: + src_doc = ezdxf.readfile(filepath) + src_msp = src_doc.modelspace() + entities = list(src_msp) + allowed_types = {"LINE", "LWPOLYLINE", "POLYLINE", "SPLINE", "CIRCLE", "ARC"} + filtered_entities = [e for e in entities if e.dxftype() in allowed_types] + entities = filtered_entities + except Exception as e: + error_msg = f"Fehler beim Lesen von {filename}: {e}" + if logger: + logger.error(error_msg) + else: + print(error_msg) + error_files.append((filename, error_msg)) + continue + + center = get_bbox(entities) + if center is None: + error_msg = f"Keine gültige Geometrie in {filename}" + if logger: + logger.error(error_msg) + else: + print(error_msg) + error_files.append((filename, error_msg)) + continue + + if name in doc.blocks: + doc.blocks.delete_block(name) + + blk = doc.blocks.new(name=name, base_point=(0,0)) + + for e in entities: + try: + cp = e.copy() + cp.translate(-center.x, -center.y, 0) # Geometrie verschieben! + blk.add_entity(cp) + except Exception as err: + error_msg = f"Fehler beim Verarbeiten von Entity {e.dxftype()} in {filename}: {err}" + if logger: + logger.error(error_msg) + else: + print(error_msg) + error_files.append((filename, error_msg)) + + # Platzierung in Reihen und Spalten + msp.add_blockref(name, insert=(x_offset, y_offset)) + # Text mit Blocknamen über dem Block + + # Werte aus Config holen (Block: [dxf2lib]) + section = "dxf2lib" + text_y = y_offset + get_cfg_value(section, "text_offset_y", DEFAULTS["text_offset_y"]) + text_offset_x = get_cfg_value(section, "text_offset_x", DEFAULTS["text_offset_x"]) + max_len = get_cfg_value(section, "max_len", DEFAULTS["max_len"]) + text_height = get_cfg_value(section, "text_height", DEFAULTS["text_height"]) + line_spacing = get_cfg_value(section, "line_spacing", DEFAULTS["line_spacing"]) + block_spacing_x = get_cfg_value(section, "block_spacing_x", DEFAULTS["block_spacing_x"]) + blocks_per_row = get_cfg_value(section, "blocks_per_row", DEFAULTS["blocks_per_row"]) + block_spacing_y = get_cfg_value(section, "block_spacing_y", DEFAULTS["block_spacing_y"]) + + if len(name) > max_len: + first_line = name[:max_len] + second_line = name[max_len:] + # obere Zeile + msp.add_text(first_line, dxfattribs={'height': text_height, 'insert': (x_offset + text_offset_x, text_y)}) + # untere Zeile, etwas niedriger (y kleiner) + msp.add_text(second_line, dxfattribs={'height': text_height, 'insert': (x_offset + text_offset_x, text_y - line_spacing)}) + else: + msp.add_text(name, dxfattribs={'height': text_height, 'insert': (x_offset + text_offset_x, text_y)}) + + processed_files.append(filename) + blocks_in_row += 1 + x_offset += block_spacing_x # Abstand zwischen Blöcken in einer Reihe + if blocks_in_row == blocks_per_row: + blocks_in_row = 0 + x_offset = 0 + y_offset -= block_spacing_y # Neue Zeile, nach unten versetzt + + if logger: + logger.info(f"Bibliotheks-DXF gespeichert: {output_file}") + logger.info(f"Verarbeitete Dateien: {len(processed_files)}") + logger.info(f"Fehlerhafte Dateien: {len(error_files)}") + + if error_files: + logger.warning(f"{len(error_files)} Dateien konnten nicht verarbeitet werden:") + for filename, error_msg in error_files: + logger.error(f"{filename}: {error_msg}") + else: + logger.info("Keine fehlerhaften Dateien gefunden.") + else: + print(f"Bibliotheks-DXF gespeichert: {output_file}") + print(f"Verarbeitete Dateien: {len(processed_files)}") + print(f"Fehlerhafte Dateien: {len(error_files)}") + + if error_files: + print(f"Warnung: {len(error_files)} Dateien konnten nicht verarbeitet werden.") + + output_dir = output_file.parent + if not output_dir.exists(): + output_dir.mkdir(parents=True, exist_ok=True) + + doc.saveas(output_file) + + +# Standardwerte (falls nicht in der Config) +DEFAULTS = { + "text_offset_y": 175, # Vertikaler Abstand des Texts über dem Block (in DXF-Einheiten) + "text_offset_x": -150, # Horizontaler Abstand des Texts links vom Block (in DXF-Einheiten) + "max_len": 9, # Maximale Zeichenlänge pro Textzeile (bei Überschreitung wird umgebrochen) + "text_height": 20, # Schriftgröße des Texts (in DXF-Einheiten) + "line_spacing": 22, # Abstand zwischen zwei Textzeilen bei Umbrüchen (in DXF-Einheiten) + "block_spacing_x": 350, # Horizontaler Abstand zwischen Blöcken in einer Reihe (in DXF-Einheiten) + "blocks_per_row": 20, # Anzahl Blöcke pro Zeile im Raster + "block_spacing_y": 500, # Vertikaler Abstand zwischen Zeilen im Raster (in DXF-Einheiten) +} + +def get_cfg_value(section, key, fallback): + try: + return int(config.get(section, key)) + except Exception: + return fallback + if __name__ == "__main__": # Argumentparser für Kommandozeilenoptionen parser = argparse.ArgumentParser(description="SVG/XML zu DXF Konverter") @@ -64,94 +235,43 @@ if __name__ == "__main__": # Verzeichnisse über Umgebungsvariablen oder Fallback if args.input: - INPUT_DIR = args.input + INPUT_DIR = Path(args.input) print(f"Verwende Input-Verzeichnis: {INPUT_DIR} \n") else: - INPUT_DIR = os.path.join(check_environment_var("PROJECT_WORK", "."), "converted_dxfs") + INPUT_DIR = check_environment_var("PROJECT_WORK") / "converted_dxfs" print(f"Kein Input-Verzeichnis angegeben, verwende Standard: {INPUT_DIR} \n") - OUTPUT_FILE = os.path.join(check_environment_var("PROJECT_DATA", "."),"block_libaries", f"{args.name}.dxf") + OUTPUT_FILE = check_environment_var("PROJECT_DATA") / "block_libaries" / f"{args.name}.dxf" - doc = ezdxf.new() - msp = doc.modelspace() + # Prüfe und erstelle log-Verzeichnis falls nötig + log_dir = check_environment_var("PROJECT_LOG") + if not log_dir.exists(): + log_dir.mkdir(parents=True, exist_ok=True) + print(f"Log-Verzeichnis erstellt: {log_dir}") - x_offset = 0 - y_offset = 0 - blocks_in_row = 0 + # Logger Setup + logger = setup_logger(log_dir, name='dxf2lib') + logger.info("=== DXF2LIB Verarbeitung gestartet ===") + logger.info(f"Input-Verzeichnis: {INPUT_DIR}") + logger.info(f"Output-Datei: {OUTPUT_FILE}") - for filename in os.listdir(INPUT_DIR): - if not filename.lower().endswith(".dxf"): - continue - - filepath = os.path.join(INPUT_DIR, filename) - name = os.path.splitext(filename)[0] - - try: - src_doc = ezdxf.readfile(filepath) - src_msp = src_doc.modelspace() - entities = list(src_msp) - allowed_types = {"LINE", "LWPOLYLINE", "POLYLINE", "SPLINE", "CIRCLE", "ARC"} - filtered_entities = [e for e in entities if e.dxftype() in allowed_types] - entities = filtered_entities - except Exception as e: - print(f"Fehler beim Lesen von {filename}: {e}") - continue - - center = get_bbox(entities) - if center is None: - print(f"Keine gültige Geometrie in {filename}") - continue - - if name in doc.blocks: - doc.blocks.delete_block(name) - - blk = doc.blocks.new(name=name, base_point=(0,0)) - - for e in entities: - try: - cp = e.copy() - cp.translate(-center.x, -center.y, 0) # Geometrie verschieben! - blk.add_entity(cp) - except Exception as err: - print(f"Fehler in {filename}: {err}") - - # Platzierung in Reihen und Spalten - msp.add_blockref(name, insert=(x_offset, y_offset)) - # Text mit Blocknamen über dem Block - text_y = y_offset + 175 # 175 Einheiten über dem Block - max_len = 9 - text_height = 20 - line_spacing = 22 # Abstand zwischen den zwei Textzeilen - - if len(name) > max_len: - first_line = name[:max_len] - second_line = name[max_len:] - # obere Zeile - msp.add_text(first_line, dxfattribs={'height': text_height, 'insert': (x_offset - 150, text_y)}) - # untere Zeile, etwas niedriger (y kleiner) - msp.add_text(second_line, dxfattribs={'height': text_height, 'insert': (x_offset - 150, text_y - line_spacing)}) - else: - msp.add_text(name, dxfattribs={'height': text_height, 'insert': (x_offset - 150, text_y)}) - blocks_in_row += 1 - x_offset += 350 # Abstand zwischen Blöcken in einer Reihe - if blocks_in_row == 20: - blocks_in_row = 0 - x_offset = 0 - y_offset -= 500 # Neue Zeile, nach unten versetzt - - print(f"Bibliotheks-DXF gespeichert: {OUTPUT_FILE} \n") - - output_dir = os.path.dirname(OUTPUT_FILE) - if not os.path.exists(output_dir): - os.makedirs(output_dir, exist_ok=True) - - doc.saveas(OUTPUT_FILE) + # Lade Config-Datei + config = configparser.ConfigParser() + config_path = check_environment_var("PROJECT_CFG") / "allgemein.cfg" + config.read(config_path, encoding="utf-8") + logger.info(f"Config-Datei geladen: {config_path}") + # Erstelle die Block-Bibliothek + logger.info(f"Erstelle Block-Bibliothek aus {INPUT_DIR}...") + create_block_library(INPUT_DIR, OUTPUT_FILE, config, logger) + if not args.keep: - print(f"Keep-Argument nicht gesetzt, lösche Input-Verzeichnis: {INPUT_DIR} \n") + logger.info(f"Keep-Argument nicht gesetzt, lösche Input-Verzeichnis: {INPUT_DIR}") try: - if os.path.exists(INPUT_DIR): + if INPUT_DIR.exists(): shutil.rmtree(INPUT_DIR) - print(f"Verzeichnis '{INPUT_DIR}' wurde gelöscht.") + logger.info(f"Verzeichnis '{INPUT_DIR}' wurde gelöscht.") except Exception as err: - print(f"[WARN] Konnte '{INPUT_DIR}' nicht löschen: {err}") + logger.warning(f"Konnte '{INPUT_DIR}' nicht löschen: {err}") + + logger.info("=== DXF2LIB Verarbeitung abgeschlossen ===") diff --git a/lib/plant2dxf.py b/lib/plant2dxf.py index 1adae2b..3fc2f4f 100644 --- a/lib/plant2dxf.py +++ b/lib/plant2dxf.py @@ -13,12 +13,21 @@ import configparser import ezdxf from pathlib import Path import math +from utils import check_environment_var, setup_logger # --------------------------------------------------------- CFG-Leser für shapes.cfg -def get_shape_cfg(teileart, cfg_path): +def get_shape_cfg(teileart, cfg_path, logger=None): parser = configparser.ConfigParser() - with open(cfg_path, encoding='utf-8') as f: - parser.read_file(f) + try: + with open(cfg_path, encoding='utf-8') as f: + parser.read_file(f) + except Exception as e: + msg = f"Fehler beim Lesen der Config-Datei {cfg_path}: {e}" + if logger: + logger.error(msg) + else: + print(msg) + return [] section = teileart if section not in parser: return [] @@ -53,14 +62,6 @@ ATTR_TAG = "TeileId" # Attributtag im Block RADIUS = 400 # Radius der Kreiselkreise (mm) # --------------------------------------------------------- Hilfsfunktionen -def check_environment_var(env_str: str) -> Path: - """Liefert Path aus Umgebungsvariable oder beendet mit Fehlermeldung.""" - out_path = os.environ.get(env_str) - if out_path: - return Path(out_path) - print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.") - sys.exit(1) - def extract_coords(planquadrat: str) -> tuple[float, float]: """Extrahiert X/Y Koordinaten aus PlanquadratString.""" m = re.search(r"X:(\d+[\.,]?\d*)\s+Y:(\d+[\.,]?\d*)", planquadrat) @@ -87,8 +88,9 @@ def import_block(block_name: str, from_doc, to_doc) -> None: for ent in src: tgt.add_entity(ent.copy()) -def berechne_hoehe(csv_path): - y_werte = [] +def berechne_hoehe(csv_path, logger=None): + y_werte = [] + try: with csv_path.open(newline="", encoding="utf-8") as fh: reader = csv.DictReader(fh, delimiter=';') for row in reader: @@ -96,11 +98,23 @@ def berechne_hoehe(csv_path): try: _, y = extract_coords(planquadrat) y_werte.append(y) - except Exception: - continue - if not y_werte: - raise ValueError("Keine Y-Koordinaten in der CSV gefunden!") - return max(y_werte) + except Exception as e: + if logger: + logger.warning(f"Fehler beim Extrahieren der Koordinate aus '{planquadrat}': {e}") + except Exception as e: + if logger: + logger.error(f"Fehler beim Lesen der CSV-Datei {csv_path}: {e}") + else: + print(f"Fehler beim Lesen der CSV-Datei {csv_path}: {e}") + return 0 + if not y_werte: + msg = "Keine Y-Koordinaten in der CSV gefunden!" + if logger: + logger.error(msg) + else: + print(msg) + raise ValueError(msg) + return max(y_werte) def transform_coords(x: float, y: float, height: float) -> tuple[float, float]: """Transformiert Bildschirmkoordinaten (0,0 oben links) ins DXF-KoSy (0,0 unten links).""" @@ -329,20 +343,45 @@ def get_libfile_cfg(teileart, cfg_path): return None # --------------------------------------------------------- Hauptfunktion -def main(csv_path: Path, default_lib_path: Path, shapes_cfg_path: Path, output_path: Path, verbose=False): +def main(csv_path: Path, lib_path: Path, cfg_path: Path, + output_path: Path, verbose=False, logger=None): - # Bibliotheks-Cache - lib_docs = {} - # Verzeichnisse - data_dir = check_environment_var("PROJECT_DATA") - blocklib_dir = Path(data_dir) / "block_libaries" + # Bibliothek nur laden, wenn Datei existiert + lib_doc = None + if lib_path.exists(): + try: + lib_doc = ezdxf.readfile(lib_path) + if verbose: + logger.info(f"[INFO] Bibliothek geladen: {lib_path}") if logger else print(f"[INFO] Bibliothek geladen: {lib_path}") + except Exception as e: + msg = f"Fehler beim Lesen der Bibliothek '{lib_path}': {e}" + if logger: + logger.error(msg) + else: + print(msg) + sys.exit(1) + else: + msg = f"[INFO] Keine Bibliothek gefunden unter {lib_path}." + if logger: + logger.warning(msg) + else: + print(msg) + sys.exit(1) - # Neue Zielzeichnung (DXF R2018) + # Neue Ziel­zeichnung (DXF R2018) doc = ezdxf.new(dxfversion="R2018") msp = doc.modelspace() # Höhe bestimmen für Koordinaten-Transformation - height = berechne_hoehe(csv_path) + try: + height = berechne_hoehe(csv_path, logger=logger) + except Exception as e: + msg = f"Fehler bei der Höhenberechnung: {e}" + if logger: + logger.error(msg) + else: + print(msg) + sys.exit(1) # Verarbeitung der Blöcke with csv_path.open(newline="", encoding="utf-8") as fh: @@ -357,7 +396,11 @@ def main(csv_path: Path, default_lib_path: Path, shapes_cfg_path: Path, output_p x_screen, y_screen = extract_coords(planquadrat) x, y = transform_coords(x_screen, y_screen, height) except Exception as e: - print(f"[WARN] {teileid}: {e}") + msg = f"[WARN] {teileid}: {e}" + if logger: + logger.warning(msg) + else: + print(msg) continue # Bibliotheksdatei bestimmen @@ -385,19 +428,26 @@ def main(csv_path: Path, default_lib_path: Path, shapes_cfg_path: Path, output_p # Funktions-Dispatch: handle_ (mit _ statt Leerzeichen und Punkten, alles klein) func_name = f'handle_{normalize_func_name(teileart)}' handler = globals().get(func_name) - symbols = get_shape_cfg(teileart, shapes_cfg_path) + symbols = get_shape_cfg(teileart, shapes_cfg_path, logger=logger) # Mapping für Omniflo-Typen if func_name.startswith('handle_omniflo'): handler = globals().get('handle_omniflo') if handler: handler(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, symbols) else: - print(f"[WARN] Keine Routine für TeileArt '{teileart}'. Überspringe '{teileid}'.") - continue + msg = f"[WARN] Keine Routine für TeileArt '{teileart}'. Überspringe '{teileid}'." + if logger: + logger.warning(msg) + else: + print(msg) + continue # DXF speichern doc.saveas(output_path) - print(f"[DONE] DXF gespeichert unter: {output_path}") + if logger: + logger.info(f"[DONE] DXF gespeichert unter: {output_path}") + else: + print(f"[DONE] DXF gespeichert unter: {output_path}") if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -410,10 +460,14 @@ if __name__ == "__main__": args = parser.parse_args() # Verzeichnisse aus Umgebungs­variablen + log_dir = check_environment_var("PROJECT_LOG") data_dir = check_environment_var("PROJECT_DATA") work_dir = check_environment_var("PROJECT_WORK") config_dir = check_environment_var("PROJECT_CFG") + logger = setup_logger(log_dir, name='plant2dxf') + logger.info("=== plant2dxf Verarbeitung gestartet ===") + # CSV‑Pfad: nur Dateiname → im WORK‑Ordner suchen if os.sep not in args.file and "/" not in args.file: csv_path = work_dir / args.file @@ -425,4 +479,5 @@ if __name__ == "__main__": default_lib_path = Path(args.lib) if args.lib else data_dir / "blocks.dxf" output_path = Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.dxf") - main(csv_path, default_lib_path, cfg_path, output_path, verbose=args.verbose) + main(csv_path, default_lib_path, cfg_path, output_path, verbose=args.verbose, logger=logger) + logger.info("=== plant2dxf Verarbeitung abgeschlossen ===") diff --git a/lib/svg2dxf.py b/lib/svg2dxf.py index d0a95a3..12950d2 100644 --- a/lib/svg2dxf.py +++ b/lib/svg2dxf.py @@ -3,14 +3,219 @@ import subprocess import sys import argparse import time -# Hilfsfunktion wie in plant2dxf.py +import ezdxf +from svg.path import parse_path, Line, Arc, CubicBezier, QuadraticBezier, Close +import xml.etree.ElementTree as ET +import re +from pathlib import Path -def check_environment_var(env_str: str, fallback: str) -> str: - out_path = os.environ.get(env_str) - if out_path: - return out_path - print(f"[WARN] Umgebungsvariable {env_str} ist nicht gesetzt oder leer. Verwende '{fallback}'.") - return fallback +from utils import check_environment_var, setup_logger + +class SVGtoDXFConverter: + def __init__(self, logger=None): + # Standard DXF-Farben (AutoCAD Color Index) + self.color_map = { + 'red': 1, + 'yellow': 2, + 'green': 3, + 'cyan': 4, + 'blue': 5, + 'magenta': 6, + 'white': 7, + 'gray': 8, + 'grey': 8, + 'black': 7 + } + self.logger = logger + + def svg_to_dxf(self, svg_file: str, dxf_file: str): + """Konvertiert eine SVG-Datei zu DXF (AutoCAD 2018)""" + try: + # DXF-Dokument für AutoCAD 2018 erstellen + doc = ezdxf.new('R2018') + msp = doc.modelspace() + + # SVG parsen + tree = ET.parse(svg_file) + root = tree.getroot() + + # SVG-Namespace definieren + namespaces = { + 'svg': 'http://www.w3.org/2000/svg', + '': 'http://www.w3.org/2000/svg' # Default namespace + } + + # Alle path-Elemente finden und konvertieren + for path_elem in root.iter(): + if path_elem.tag.endswith('path') or path_elem.tag == 'path': + self.convert_path_element(path_elem, msp) + + # DXF speichern + doc.saveas(dxf_file) + if self.logger: + self.logger.info(f"DXF-Datei erfolgreich erstellt: {dxf_file}") + else: + print(f"DXF-Datei erfolgreich erstellt: {dxf_file}") + except Exception as e: + if self.logger: + self.logger.error(f"Fehler beim Parsen der SVG-Datei {svg_file}: {e}") + else: + print(f"Fehler beim Parsen der SVG-Datei {svg_file}: {e}") + + def convert_path_element(self, path_elem, msp): + """Konvertiert ein SVG path-Element zu DXF-Entities""" + d = path_elem.get('d') + if not d: + return + + # Farbe aus stroke-Attribut extrahieren + color_index = self.get_color_from_stroke(path_elem.get('stroke', 'black')) + + try: + path = parse_path(d) + self.convert_path_to_dxf(path, msp, color_index) + except Exception as e: + if self.logger: + self.logger.warning(f"Fehler beim Parsen des Pfads: {e}") + else: + print(f"Warnung: Fehler beim Parsen des Pfads: {e}") + + def get_color_from_stroke(self, stroke_value: str) -> int: + """Konvertiert SVG stroke-Farbe zu DXF Color Index""" + if not stroke_value or stroke_value == 'none': + return 7 # Standard: weiß/schwarz + + stroke_value = stroke_value.lower().strip() + + # Direkte Farbnamen + if stroke_value in self.color_map: + return self.color_map[stroke_value] + + # Hex-Farben zu RGB und dann zu nächster DXF-Farbe + if stroke_value.startswith('#'): + return self.hex_to_dxf_color(stroke_value) + + # RGB-Farben + rgb_match = re.match(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)', stroke_value) + if rgb_match: + r, g, b = map(int, rgb_match.groups()) + return self.rgb_to_dxf_color(r, g, b) + + return 7 # Fallback: weiß/schwarz + + def hex_to_dxf_color(self, hex_color: str) -> int: + """Konvertiert Hex-Farbe zu DXF Color Index""" + hex_color = hex_color.lstrip('#') + if len(hex_color) == 6: + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + return self.rgb_to_dxf_color(r, g, b) + return 7 + + def rgb_to_dxf_color(self, r: int, g: int, b: int) -> int: + """Konvertiert RGB-Werte zu nächstem DXF Color Index""" + # Vereinfachte Zuordnung zu Hauptfarben + if r > 200 and g < 100 and b < 100: + return 1 # Rot + elif r > 200 and g > 200 and b < 100: + return 2 # Gelb + elif r < 100 and g > 200 and b < 100: + return 3 # Grün + elif r < 100 and g > 200 and b > 200: + return 4 # Cyan + elif r < 100 and g < 100 and b > 200: + return 5 # Blau + elif r > 200 and g < 100 and b > 200: + return 6 # Magenta + elif r > 200 and g > 200 and b > 200: + return 7 # Weiß + else: + return 8 # Grau + + def convert_path_to_dxf(self, path, msp, color_index: int): + """Konvertiert SVG-Pfad zu DXF-Entities""" + current_point = None + + for segment in path: + if isinstance(segment, Line): + start = (segment.start.real, -segment.start.imag) # Y-Achse umkehren + end = (segment.end.real, -segment.end.imag) + line = msp.add_line(start, end) + line.dxf.color = color_index + current_point = end + + elif isinstance(segment, Arc): + # Arc zu Polyline approximieren + points = self.arc_to_points(segment) + if len(points) > 1: + for i in range(len(points) - 1): + start = (points[i].real, -points[i].imag) + end = (points[i+1].real, -points[i+1].imag) + line = msp.add_line(start, end) + line.dxf.color = color_index + current_point = (points[-1].real, -points[-1].imag) + + elif isinstance(segment, (CubicBezier, QuadraticBezier)): + # Bezier-Kurven zu Polyline approximieren + points = self.bezier_to_points(segment) + if len(points) > 1: + for i in range(len(points) - 1): + start = (points[i].real, -points[i].imag) + end = (points[i+1].real, -points[i+1].imag) + line = msp.add_line(start, end) + line.dxf.color = color_index + current_point = (points[-1].real, -points[-1].imag) + + elif isinstance(segment, Close): + # Pfad schließen - zurück zum Startpunkt + if current_point and len(path) > 0: + start_point = (path[0].start.real, -path[0].start.imag) + if current_point != start_point: + line = msp.add_line(current_point, start_point) + line.dxf.color = color_index + + def arc_to_points(self, arc, num_points: int = 20): + """Approximiert einen Bogen durch Punkte""" + points = [] + for i in range(num_points + 1): + t = i / num_points + point = arc.point(t) + points.append(point) + return points + + def bezier_to_points(self, bezier, num_points: int = 20): + """Approximiert eine Bezier-Kurve durch Punkte""" + points = [] + for i in range(num_points + 1): + t = i / num_points + point = bezier.point(t) + points.append(point) + return points + +# Batch-Konvertierung für mehrere Dateien +def batch_convert(svg_files: list, output_dir: str = '.', logger=None): + """Konvertiert mehrere SVG-Dateien""" + output_dir = Path(output_dir) # Sicherstellen, dass es ein Path ist + converter = SVGtoDXFConverter(logger=logger) + + for svg_file in svg_files: + try: + # Ausgabedateiname generieren + base_name = Path(svg_file).stem + dxf_file = output_dir / f"{base_name}.dxf" + + converter.svg_to_dxf(svg_file, dxf_file) + if logger: + logger.info(f"✓ {svg_file} -> {dxf_file}") + else: + print(f"✓ {svg_file} -> {dxf_file}") + + except Exception as e: + if logger: + logger.error(f"✗ Fehler bei {svg_file}: {e}") + else: + print(f"✗ Fehler bei {svg_file}: {e}") if __name__ == "__main__": @@ -18,7 +223,8 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="SVG/XML zu DXF Konverter") parser.add_argument('-i', '--input', type=str, help='Input-Verzeichnis mit SVG/XML-Dateien') parser.add_argument('-o', '--output', type=str, help='Output-Verzeichnis für DXF-Dateien') - + parser.add_argument('-k', '--inkscape', action='store_true', help='Batch-Konvertierung über Inkscape') + if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): parser.print_help() sys.exit(0) @@ -27,37 +233,57 @@ if __name__ == "__main__": # Verzeichnisse über Umgebungsvariablen oder Fallback if args.input: - INPUT_DIR = args.input + INPUT_DIR = Path(args.input) print(f"Input-Verzeichnis: {INPUT_DIR} \n") else: - INPUT_DIR = os.path.join(check_environment_var("PROJECT_DATA", "."), "svg") + INPUT_DIR = check_environment_var("PROJECT_DATA") / "svg" print(f"Kein Input-Verzeichnis angegeben, verwende Standard: {INPUT_DIR} \n") if args.output: - OUTPUT_DIR = args.output + OUTPUT_DIR = Path(args.output) print(f"Output-Verzeichnis: {OUTPUT_DIR} \n") else: - OUTPUT_DIR = os.path.join(check_environment_var("PROJECT_WORK", "."), "converted_dxfs") + OUTPUT_DIR = check_environment_var("PROJECT_WORK") / "converted_dxfs" print(f"Kein Output-Verzeichnis angegeben, verwende Standard: {OUTPUT_DIR} \n") - INKSCAPE = "inkscape" # ggf. vollständiger Pfad angeben - - os.makedirs(OUTPUT_DIR, exist_ok=True) + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) - files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith((".svg", ".xml"))] + files = [f for f in INPUT_DIR.iterdir() if f.suffix.lower() in (".svg", ".xml")] + files = [str(f) for f in files] # batch_convert erwartet str + + log_dir = check_environment_var("PROJECT_LOG") + logger = setup_logger(log_dir, name='svg2dxf') + logger.info("=== SVG2DXF Verarbeitung gestartet ===") + logger.info(f"Input-Verzeichnis: {INPUT_DIR}") + logger.info(f"Output-Verzeichnis: {OUTPUT_DIR}") + + + if args.inkscape: + INKSCAPE = "inkscape" # ggf. vollständiger Pfad angeben + for filename in files: + input_path = INPUT_DIR / filename + output_path = OUTPUT_DIR / (Path(filename).stem + ".dxf") + if logger: + logger.info(f"Konvertiere: {input_path} → {output_path}") + else: + print(f"Konvertiere: {input_path} → {output_path}") + try: + subprocess.run([ + INKSCAPE, + str(input_path), + "--export-type=dxf", + f"--export-filename={output_path}" + ], check=True) + except subprocess.CalledProcessError as e: + if logger: + logger.error(f"[FEHLER] Inkscape-Absturz bei {filename}: {e}") + else: + print(f"[FEHLER] Inkscape-Absturz bei {filename}: {e}") + time.sleep(0.5) # kleine Pause zur Entlastung von RAM/GTK + + else: + batch_convert(files, str(OUTPUT_DIR), logger=logger) + + logger.info("=== SVG2DXF Verarbeitung abgeschlossen ===") - for filename in files: - input_path = os.path.join(INPUT_DIR, filename) - output_path = os.path.join(OUTPUT_DIR, os.path.splitext(filename)[0] + ".dxf") - print(f"Konvertiere: {input_path} → {output_path}") - try: - subprocess.run([ - INKSCAPE, - input_path, - "--export-type=dxf", - f"--export-filename={output_path}" - ], check=True) - except subprocess.CalledProcessError as e: - print(f"[FEHLER] Inkscape-Absturz bei {filename}: {e}") - time.sleep(0.5) # kleine Pause zur Entlastung von RAM/GTK diff --git a/lib/utils.py b/lib/utils.py new file mode 100644 index 0000000..daf0326 --- /dev/null +++ b/lib/utils.py @@ -0,0 +1,56 @@ +import os +import sys +import logging +from pathlib import Path + + +def check_environment_var(env_str: str) -> Path: + """Liefert Path aus Umgebungsvariable oder beendet mit Fehlermeldung.""" + out_path = os.environ.get(env_str) + if out_path: + return Path(out_path) + msg = f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer." + print(msg) + sys.exit(1) + + +def setup_logger(log_dir: Path, name: str = 'app') -> logging.Logger: + """ + Erstellt und konfiguriert einen Logger, der sowohl in eine Datei als auch auf die Konsole schreibt. + Die Logdatei erhält einen Zeitstempel im Namen und ist UTF-8 kodiert. + + Args: + log_dir (Path): Verzeichnis für die Log-Dateien + name (str): Name des Loggers (z.B. 'plant2dxf', 'dxf2lib') + Returns: + logging.Logger: Konfigurierter Logger + """ + from datetime import datetime + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + + # Vorherige Handler entfernen, um doppelte Logs zu vermeiden + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + # Log-Verzeichnis anlegen (falls nicht vorhanden) + log_dir.mkdir(parents=True, exist_ok=True) + + # Logdatei mit Zeitstempel im Namen erzeugen + log_file = log_dir / f"{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" + file_handler = logging.FileHandler(log_file, encoding='utf-8') + file_handler.setLevel(logging.INFO) + + # Handler für die Konsole + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + + # Einheitliches Log-Format definieren + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # Handler dem Logger hinzufügen + logger.addHandler(file_handler) + logger.addHandler(console_handler) + return logger \ No newline at end of file