Files
plant2dxf/lib/dxf2lib.py
T
m.stangl c38b9861d4 - Anpassungen an Konfigurations- und Bibliotheksdateien vorgenommen
- logging in dxf2lib.py implementiert
- Umgebungsvariablen in setenv.bat für log-Verzeichnis und Bibliothek-Pfad hinzugefügt
- Bibliothek-Pfad in plant2dxf.py angepasst
2025-07-30 15:23:07 +02:00

343 lines
13 KiB
Python

import os
import ezdxf
from ezdxf.math import Vec2
import argparse
import sys
import shutil
import datetime
import configparser
import logging
def check_environment_var(env_str: str, fallback: str) -> str:
"""
Prüft eine Umgebungsvariable und gibt den Wert oder einen Fallback zurück.
Args:
env_str (str): Name der zu prüfenden Umgebungsvariable
fallback (str): Fallback-Wert, falls die Variable nicht gesetzt ist
Returns:
str: Wert der Umgebungsvariable oder Fallback-Wert
Note:
Gibt eine Warnung aus, falls die Umgebungsvariable nicht gesetzt ist
"""
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:
try:
if e.dxftype() == "POLYLINE":
for vertex in e.vertices:
if hasattr(vertex.dxf, 'location'):
x, y = vertex.dxf.location.x, vertex.dxf.location.y
min_x, min_y = min(min_x, x), min(min_y, y)
max_x, max_y = max(max_x, x), max(max_y, y)
elif e.dxftype() == "LWPOLYLINE":
for x, y, *_ in e.get_points("xy"):
min_x, min_y = min(min_x, x), min(min_y, y)
max_x, max_y = max(max_x, x), max(max_y, y)
else:
# Für andere Entity-Typen: Standard bbox() verwenden
box = e.bbox()
if box:
(x1, y1), (x2, y2) = box.extmin, box.extmax
min_x, min_y = min(min_x, x1), min(min_y, y1)
max_x, max_y = max(max_x, x2), max(max_y, y2)
except Exception as err:
print(f" BBox Fehler für {e.dxftype()}: {err}")
continue
if min_x == float('inf'):
return None
return Vec2((min_x + max_x) / 2, (min_y + max_y) / 2)
def setup_logger(log_dir):
"""
Erstellt und konfiguriert einen Logger für die DXF2LIB Verarbeitung.
Args:
log_dir (str): Verzeichnis für die Log-Dateien
Returns:
logging.Logger: Konfigurierter Logger
Note:
Erstellt eine Log-Datei mit Zeitstempel und formatiert die Ausgaben
"""
# Logger erstellen
logger = logging.getLogger('dxf2lib')
logger.setLevel(logging.INFO)
# Bestehende Handler entfernen (falls Logger bereits existiert)
for handler in logger.handlers[:]:
logger.removeHandler(handler)
# Zeitstempel für Log-Datei
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = os.path.join(log_dir, f"dxf2lib_{timestamp}.log")
# File Handler erstellen
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.INFO)
# Console Handler erstellen
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
# Formatter erstellen
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
console_handler.setFormatter(formatter)
# Handler zum Logger hinzufügen
logger.addHandler(file_handler)
logger.addHandler(console_handler)
return logger, log_file
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 = os.path.dirname(output_file)
if not os.path.exists(output_dir):
os.makedirs(output_dir, 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")
parser.add_argument('-i', '--input', type=str, help='Input-Verzeichnis mit SVG/XML-Dateien')
parser.add_argument('-n', '--name', required=False, type=str, help='Name der zu erzeugenden Bibliothek (optional, wird sonst abgefragt)')
parser.add_argument('-k', '--keep', action="store_true", help='Behalte converted_dxf Ordner. Sonst Löschen nach erfolgreicher Bibliothekserstellung')
if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"):
parser.print_help()
sys.exit(0)
args = parser.parse_args()
if not args.name:
args.name = input("Bitte Namen der zu erzeugenden Bibliothek eingeben: ").strip()
if not args.name:
print("Fehler: Kein Name angegeben. Beende.")
sys.exit(1)
# Verzeichnisse über Umgebungsvariablen oder Fallback
if args.input:
INPUT_DIR = args.input
print(f"Verwende Input-Verzeichnis: {INPUT_DIR} \n")
else:
INPUT_DIR = os.path.join(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")
# Prüfe und erstelle log-Verzeichnis falls nötig
log_dir = check_environment_var("PROJECT_LOG", ".")
if not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True)
print(f"Log-Verzeichnis erstellt: {log_dir}")
# Logger Setup
logger, log_file = setup_logger(log_dir)
logger.info("=== DXF2LIB Verarbeitung gestartet ===")
logger.info(f"Input-Verzeichnis: {INPUT_DIR}")
logger.info(f"Output-Datei: {OUTPUT_FILE}")
# Lade Config-Datei
config = configparser.ConfigParser()
config_path = os.path.join(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:
logger.info(f"Keep-Argument nicht gesetzt, lösche Input-Verzeichnis: {INPUT_DIR}")
try:
if os.path.exists(INPUT_DIR):
shutil.rmtree(INPUT_DIR)
logger.info(f"Verzeichnis '{INPUT_DIR}' wurde gelöscht.")
except Exception as err:
logger.warning(f"Konnte '{INPUT_DIR}' nicht löschen: {err}")
logger.info("=== DXF2LIB Verarbeitung abgeschlossen ===")