client/parser/main.py

951 lines
40 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import os
import traceback
import time
import re
from collections import defaultdict
from win32com.client import Dispatch, gencache
from PIL import Image, ImageDraw, ImageFont
import xmltodict
import pythoncom
from win32com.client import Dispatch, VARIANT
from enums import KompasCommand
from logger import logger
ids = {
"api_5": "{0422828C-F174-495E-AC5D-D31014DBBE87}",
"api_7": "{69AC2981-37C0-4379-84FD-5DD2F3C0A520}",
"const": "{75C9F5D0-B5B8-4526-8681-9903C567D2ED}",
}
class KompasDocumentParser:
def __init__(self):
self.api5 = None
self.api7 = None
self.constants = None
self.application = None
self._init_kompas()
def _init_kompas(self):
"""Инициализация API КОМПАС версий 5 и 7"""
try:
import pythoncom
pythoncom.CoInitialize()
# Получаем API версии 5
self.api5_module = gencache.EnsureModule(ids["api_5"], 0, 1, 0)
self.api5 = self.api5_module.KompasObject(
Dispatch("Kompas.Application.5")._oleobj_.QueryInterface(
self.api5_module.KompasObject.CLSID, pythoncom.IID_IDispatch
)
)
# Получаем API версии 7
self.api7_module = gencache.EnsureModule(ids["api_7"], 0, 1, 0)
self.api7 = self.api7_module.IKompasAPIObject(
Dispatch("Kompas.Application.7")._oleobj_.QueryInterface(
self.api7_module.IKompasAPIObject.CLSID, pythoncom.IID_IDispatch
)
)
# Получаем константы
constants_module = gencache.EnsureModule(ids["const"], 0, 1, 0)
self.constants = constants_module.constants
# Правильное получение IApplication
self.application = self.api7_module.IApplication(self.api7)
self.application.Visible = True
except Exception as e:
# Выводим полный traceback и сообщение об ошибке
print("[ERROR] Ошибка при подключении к КОМПАС:")
traceback.print_exc() # <-- Выводит номер строки, файл и стек
raise RuntimeError(f"Ошибка при подключении к КОМПАС: {e}")
def get_export_path(self, doc_path: str, doc_name: str, ext: str) -> str:
"""
Формирует путь для экспорта файла по шаблону:
- Создаёт подпапку `ext` внутри `doc_path`
- Возвращает полный путь к файлу `<doc_name>.<ext>`
"""
output_dir = os.path.join(doc_path, ext)
filename = f"{doc_name}.{ext}"
full_path = os.path.join(output_dir, filename)
return full_path
def prepare_export_path(self, doc_path: str, doc_name: str, ext: str) -> str:
full_path = self.get_export_path(doc_path, doc_name, ext)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
return full_path
def get_open_documents(self):
"""Возвращает список информации о всех открытых документах"""
documents = []
docs_collection = self.application.Documents
for i in range(docs_collection.Count):
try:
doc = docs_collection.Item(i) # Индексация с 1
if doc is None:
continue
documents.append(
{
"type": doc.DocumentType,
"name": doc.Name,
"path": doc.Path,
"active": doc.Active,
}
)
except Exception as e:
print(f"Ошибка при обработке документа #{i + 1}: {e}")
return documents
def create_drawing_for_parts(self):
"""
Создание чертежей для всех уникальных деталей из открытых деталей и сборок.
Также создаёт спецификацию для сборок.
"""
logger.info("Начинаем создание чертежей для деталей...")
result = {"result": []}
# Получаем доступные типы из разрешённых действий
av_actions = self.get_available_actions()
allowed_types = av_actions[KompasCommand.PROJECT_SUPPORT]["allowed_types"]
docs_collection = self.application.Documents
for i in range(docs_collection.Count):
try:
doc = docs_collection.Item(i)
if doc is None:
continue
doc_type = doc.DocumentType
if doc_type not in allowed_types:
continue
doc.Active = True
doc_path = doc.Path
doc_name = "-".join(doc.Name.split(".")[:-1])
logger.info(f"Обрабатываем документ: {doc_name}")
doc_3d = self.api7_module.IKompasDocument3D(doc)
top_part = doc_3d.TopPart
# Используем общую рекурсивную функцию вместо all_elements
if doc_type == self.constants.ksDocumentAssembly:
result_recursive = self._traverse_parts_recursive(top_part)
elements = result_recursive["elements"]
welding = result_recursive["welding"]
bends = result_recursive["bends"]
else:
# Для отдельной детали просто собираем данные
elements = [top_part]
welding = self._collect_welds_from_drawing(top_part)
bends = self._collect_bends_from_element(top_part)
logger.info(f"Найдено элементов для создания чертежей: {len(elements)}")
# Сохранение чертежей
drawings = []
for component in elements:
component_ipart = self.api7_module.IPart7(component)
if component_ipart.Standard:
continue
logger.info(f"Создаём чертёж для: {component.Name}")
# Создаём новый чертёж
c_doc = self.application.Documents.Add(
self.constants.ksDocumentDrawing
)
c_layout = c_doc.LayoutSheets
c_sheet = c_layout.Item(0)
c_sheet.Format.Format = self.constants.ksFormatA3
c_sheet.Format.VerticalOrientation = False
c_sheet.Update()
# Расчёт габаритов
c_size = [1, 1, 1]
if component_ipart.Owner.ResultBodies:
gabarit = [0, 0, 0, 0, 0, 0]
if hasattr(component_ipart.Owner.ResultBodies, "GetGabarit"):
component_ipart.Owner.ResultBodies.GetGabarit(*gabarit)
g1 = gabarit[1:4]
g2 = gabarit[4:]
c_size = [abs(g1[i] - g2[i]) for i in range(len(g1))]
# Масштабирование
c_scale = (
c_sheet.Format.FormatHeight / sum(c_size)
if sum(c_size) > 0
else 1
)
# Получаем интерфейс 2D документа
c_doc_2d = self.api7_module.IKompasDocument2D(c_doc)
c_views = c_doc_2d.ViewsAndLayersManager.Views
# Добавляем стандартные виды
c_views.AddStandartViews(
component_ipart.FileName,
component_ipart.Name,
[1, 3, 5, 7],
c_size[1] * c_scale,
c_sheet.Format.FormatHeight - 25,
c_scale,
20,
20,
)
filename = "_".join(
filter(
None, [component_ipart.Marking, component_ipart.Name[:20]]
)
).replace(" ", "-")
full_path = self.prepare_export_path(doc_path, filename, "cdw")
c_doc.SaveAs(full_path)
logger.info(f"[OK] Сохранён чертёж: {full_path}")
drawings.append(
{
"name": f"{filename}.cdw",
"path": full_path,
"success": True,
"timestamp": time.time(),
}
)
# Сохранение спецификации для сборок
specifications = []
if doc_type == self.constants.ksDocumentAssembly:
spec = self.application.Documents.Add(
self.constants.ksDocumentSpecification
)
spec_doc = self.api7_module.ISpecificationDocument(spec)
spec_doc.AttachedDocuments.Add(doc.PathName, True)
filename = "Список_деталей"
full_path = self.prepare_export_path(doc_path, filename, "spw")
spec.SaveAs(full_path)
logger.info(f"[OK] Сохранена спецификация: {full_path}")
specifications.append(
{
"name": f"{filename}.spw",
"path": full_path,
"success": True,
"timestamp": time.time(),
}
)
# Добавляем результат
result["result"].append(
{
"document_name": doc_name,
"drawings": drawings,
"specifications": specifications,
"success": len(drawings) > 0 or len(specifications) > 0,
"timestamp": time.time(),
}
)
except Exception as e:
logger.error(
f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}",
exc_info=True,
)
result["result"].append(
{
"document_name": f"Документ #{i + 1}",
"drawings": [],
"specifications": [],
"success": False,
"error": str(e),
"timestamp": time.time(),
}
)
return result
def save_to_iges(self):
"""Сохраняет открытые 3D-документы (детали/сборки) в формате IGES"""
logger.info("Начинаем сохранение документов в формате IGES...")
result = {"result": []}
av_actions = self.get_available_actions()
docs_collection = self.application.Documents
allowed_types = av_actions[KompasCommand.IGES]["allowed_types"]
for i in range(docs_collection.Count):
try:
doc = docs_collection.Item(i)
if doc is None:
continue
doc_type = doc.DocumentType
if doc_type not in allowed_types:
continue
doc.Active = True
doc_path = doc.Path
doc_name = "-".join(doc.Name.split(".")[:-1])
logger.info(f"Попытка сохранить: {doc_name}")
# Получаем 3D-документ через API v5
doc_api5 = self.api5.ActiveDocument3D()
if not doc_api5:
raise RuntimeError(
"Не удалось получить активный 3D-документ через API v5"
)
# Подготавливаем параметры сохранения в IGES
save_params = doc_api5.AdditionFormatParam()
save_params.Init()
save_params.format = self.constants.ksConverterToIGES
full_path = self.prepare_export_path(doc_path, doc_name, "igs")
export_success = doc_api5.SaveAsToAdditionFormat(full_path, save_params)
# Добавляем информацию о результате в массив
logger.info(f"Успешно сохранено: {full_path}")
result["result"].append(
{
"file": full_path,
"success": bool(export_success),
"document_name": doc_name,
"document_type": doc_type,
"timestamp": time.time(),
}
)
if not export_success:
logger.error(f"Не удалось сохранить: {full_path}")
except Exception as e:
logger.error(
f"Ошибка при обработке документа #{i + 1}: {e}",
exc_info=True,
)
result["result"].append(
{
"file": None,
"success": False,
"error": str(e),
"document_index": i + 1,
"timestamp": time.time(),
}
)
return result
def _collect_bends_from_element(self, element):
bends = []
try:
feature = self.api7_module.IFeature7(element)
sub_features = feature.SubFeatures(1, True, False) or []
for item in sub_features:
if type(item) in (
self.api7_module.ISheetMetalBend,
self.api7_module.ISheetMetalLineBend,
self.api7_module.ISheetMetalBody,
):
sub_sheets = item.Owner.SubFeatures(1, True, False)
if sub_sheets:
for b in sub_sheets:
bend = self.api7_module.ISheetMetalBend(b)
bends.append(bend)
except Exception as e:
logger.error("Ошибка при сборе гибов из элемента", exc_info=True)
return bends
def _collect_welds_from_drawing(self, part):
welding = []
try:
drawing_context = self.api7_module.IDrawingContainer(part)
macro = self.api7_module.IMacroObject3D(drawing_context)
sub_features = macro.Owner.SubFeatures(1, True, False) or []
for item in sub_features:
if isinstance(item, self.api7_module.IUserDesignationCompObj):
welding.append(item)
except Exception as e:
logger.error(
"Ошибка при сборе сварок из чертежного контекста", exc_info=True
)
return welding
def _traverse_parts_recursive(self, part):
elements = []
welding = []
bends = []
try:
# Собираем сварки
welding += self._collect_welds_from_drawing(part)
# Обходим подчасти
doc_parts = self.api7_module.IParts7(part.Parts)
for j in range(doc_parts.Count):
element = doc_parts.Part(j)
# if element.Parts.Count == 0:
# elements.append(element)
elements.append(element)
bends += self._collect_bends_from_element(element)
result = self._traverse_parts_recursive(element)
elements += result["elements"]
welding += result["welding"]
bends += result["bends"]
except Exception as e:
logger.error("Ошибка при рекурсивном обходе частей", exc_info=True)
return {"elements": elements, "welding": welding, "bends": bends}
def _build_statistics_data(self, elements, welding):
stats = {"Name": {}, "Material": {}, "Area": {}}
item_template = lambda: {"quantity": 0, "area": 0, "mass": 0, "material": ""}
detail_stats = {
"standard": defaultdict(item_template),
"custom": defaultdict(item_template),
}
# Сбор данных по элементам (кроме "Welding")
for e in elements:
type_key = "standard" if e.Standard else "custom"
name = getattr(e, "Name", "Неизвестно")
material = getattr(e, "Material", "Неизвестно")
element_key = (name, material)
mass_inertial_params = self.api7_module.IMassInertiaParam7(e)
area = mass_inertial_params.Area * 0.0001
mass = round(getattr(e, "Mass"), 3)
quantity = 1
detail_stats[type_key][element_key]["material"] = material
detail_stats[type_key][element_key]["quantity"] += quantity
detail_stats[type_key][element_key]["area"] += area
detail_stats[type_key][element_key]["mass"] += mass
for key in stats.keys():
if key == "Name":
value = f"{getattr(e, key)}, масса {round(getattr(e, 'Mass'), 3)}"
stats[key][value] = stats[key].get(value, 0) + 1
elif key == "Area":
mass_inertial_params = self.api7_module.IMassInertiaParam7(e)
area = mass_inertial_params.Area * 0.0001 # м²
material = getattr(e, "Material", "Неизвестно")
key_area = f"Площадь {material}, м²:"
stats[key][key_area] = round(stats[key].get(key_area, 0) + area, 6)
else:
try:
value = getattr(e, key)
stats[key][value] = stats[key].get(value, 0) + 1
except AttributeError:
logger.warning(f"Элемент не имеет атрибута '{key}', пропускаем")
# logger.info(detail_stats)
# Общая площадь
stats["Area"]["Total"] = sum(stats["Area"].values())
# Добавляем сварки в статистику как отдельный раздел
stats["Welding"] = {}
for w in welding:
w_name_split = w.Name.split("-")
w_len = w_name_split[-1].split("@")[0]
stats["Welding"][w.Name] = w_len
if stats["Welding"]:
total_length = sum(
float(w_len)
for w_len in stats["Welding"].values()
if w_len.replace(".", "", 1).isdigit()
)
stats["Welding"]["Total"] = round(total_length, 2)
return stats
def extract_section(name):
"""Извлечение сечения из имени профиля"""
match = re.search(r"(\d+)х(\d+)(?:х(\d+))?", name)
if match:
w, h, t = match.groups()
return f"{w}x{h}" + (f", толщ. {t}" if t else "")
return "Без сечения"
def classify_item(self, name, obj):
"""Классификация элементов по типу и имени"""
name_lower = name.lower()
cls_name = str(type(obj))
# Гиб листовой детали
if isinstance(
obj,
(self.api7_module.ISheetMetalBend, self.api7_module.ISheetMetalLineBend),
):
return "Гиб"
# Сварной шов
elif "IUserDesignationCompObj" in cls_name or (
"IUserObject3D" in cls_name and "свар" in name_lower
):
return "Сварной шов"
# Стандартное изделие
elif "IMacroObject3D" in cls_name or any(
kw in name_lower for kw in ["болт", "гайка", "шайба"]
):
return "Стандартное изделие"
# Профильная труба
elif "IUserObject3D" in cls_name and any(
kw in name_lower for kw in ["труба", "профиль"]
):
return "Профильная труба"
# Листовая оболочка
elif "ISheetMetalRuledShell" in cls_name:
return "Листовая деталь"
# Обычная деталь
elif "IPart7" in cls_name:
return "Кастомная деталь"
# Вспомогательные объекты
elif any(
kw in cls_name
for kw in [
"IPlane3D",
"IAxis3D",
"IModelObject",
"IMateConstraint3D",
"ISketch",
]
):
return "Вспомогательный объект"
else:
return "Неизвестный тип"
def merge_results(self, target, source):
for category in ["Standard", "Sheet", "Pipe"]:
for key, items in source.get(category, {}).items():
target[category].setdefault(key, []).extend(items)
return target
def traverse_ipart(self, element, depth=0, viewed=None, result=None):
if viewed is None:
viewed = set()
obj_id = id(element)
if obj_id in viewed:
return
viewed.add(obj_id)
if result is None:
result = {"Standard": {}, "Sheet": {}, "Pipe": {}, "Body": {}, "Weld": {}}
property_manager = self.api7_module.IPropertyMng(self.application)
parts_collection = self.api7_module.IPart7(element).Parts
if not isinstance(parts_collection, tuple):
parts_collection = (parts_collection,)
sub_features = self.api7_module.IFeature7(element).SubFeatures(1, True, True)
if not isinstance(sub_features, tuple):
sub_features = (sub_features,)
bodies = self.api7_module.IFeature7(element).ResultBodies
if not isinstance(bodies, tuple):
bodies = (bodies,)
for f in parts_collection + sub_features + bodies:
unique_key = id(f)
if unique_key in viewed:
continue
viewed.add(unique_key)
print(f"Обрабатываем: {type(f)} - {getattr(f, 'Name', 'Без имени')}")
if getattr(f, "Standard", False):
material = getattr(f, "Material", "Без материала")
key = " / ".join([f.Name, material])
entry = {"name": f.Name}
result["Standard"].setdefault(key, []).append(entry)
elif getattr(f, "FileName", None):
param = f.GetOpenDocumentParam()
param.Visible = True
param.ReadOnly = True
new_doc3d = f.OpenSourceDocument(param)
new_doc = self.api7_module.IKompasDocument(new_doc3d)
new_doc.Active = True
new_doc3d = self.api7_module.IKompasDocument3D(new_doc)
new_result = self.traverse_ipart(new_doc3d.TopPart)
self.merge_results(result, new_result)
new_doc.Close(self.constants.kdDoNotSaveChanges)
else:
if isinstance(f, self.api7_module.IBody7):
# Получаем интерфейс IPropertyKeeper для текущего тела
property_keeper = self.api7_module.IPropertyKeeper(f)
# Получаем свойства "Длина профиля" и "Сечение"
prop_length = property_manager.GetProperty(
self.application.ActiveDocument, "Длина профиля"
)
prop_section = property_manager.GetProperty(
self.application.ActiveDocument, "Сечение"
)
# Извлекаем значения свойств
success_length, value_length, _ = property_keeper.GetPropertyValue(
prop_length, "", True, True
)
success_section, value_section, _ = (
property_keeper.GetPropertyValue(prop_section, "", True, True)
)
# Если оба свойства успешно извлечены — сохраняем в результат
if success_section and success_length:
key_str = str(value_section) # Используем сечение как ключ
entry = {
"length": round(value_length, 6),
"section": value_section,
}
result["Pipe"].setdefault(key_str, []).append(entry)
else:
name = getattr(f, "Name", "Без Имени")
material = getattr(f, "Material", "Неизвестный материал")
key_str = material
mass_inertia = self.api7_module.IMassInertiaParam7(f)
mass = mass_inertia.Mass * 0.001
entry = {
"mass": round(mass, 6),
"name": name,
}
result["Body"].setdefault(key_str, []).append(entry)
if isinstance(f, self.api7_module.IUserDesignationCompObj):
weldes = self.api7_module.IFeature7(f).SubFeatures(1, True, True)
user_params = self.api7_module.IUserParameters(f)
lib_filename = user_params.LibraryName
if lib_filename == "Welding3D":
prop = property_manager.GetProperty(
self.application.ActiveDocument, "Длина сварного шва"
)
for w in weldes:
property_keeper = self.api7_module.IPropertyKeeper(w)
res, value, from_source = property_keeper.GetPropertyValue(
prop, "", True, True
)
if res:
key_str = w.Name
entry = {
"length": round(value, 6),
}
result["Weld"].setdefault(key_str, []).append(entry)
if isinstance(
f,
(
self.api7_module.ISheetMetalBend,
self.api7_module.ISheetMetalRuledShell,
self.api7_module.ISheetMetalBody,
),
):
body = self.api7_module.ISheetMetalBody(f)
mass_inertia = self.api7_module.IMassInertiaParam7(body.Part)
area = mass_inertia.Area * 0.0001 # м²
mass = mass_inertia.Mass * 0.001 # кг
radius = getattr(f, "Radius", 0)
if not hasattr(f, 'Radius'):
radius = getattr(body, "Radius", 0)
material = getattr(body.Part, "Material", "Неизвестный материал")
thickness = getattr(body, "Thickness", 0)
name = getattr(f, "Name", "Без имени")
key_str = f"{material} / {thickness:.1f}"
sheets = self.api7_module.IFeature7(f).SubFeatures(1, True, True)
entry = {
"radius": round(radius, 6),
"area": round(area, 6),
"mass": round(mass, 6),
"name": name,
}
for s in sheets:
entry["name"] = (", ".join([s.Owner.Name, entry["name"]]))
result["Sheet"].setdefault(key_str, []).append(entry)
if isinstance(
f,
(
self.api7_module.IPart7,
self.api7_module.IBody7,
self.api7_module.IFeature7,
),
):
self.traverse_ipart(f, depth + 1, viewed, result)
return result
def normalize_keys(self, data, repl=" / "):
"""
Рекурсивно преобразует все кортежи в ключах словарей в строки.
Все элементы кортежа приводятся к строке перед объединением.
"""
if isinstance(data, dict):
normalized = {}
for key, value in data.items():
# Если ключ — кортеж, конвертируем каждый элемент в строку
if isinstance(key, tuple):
new_key = repl.join(str(x) for x in key)
else:
new_key = key
normalized[new_key] = self.normalize_keys(value, repl)
return normalized
elif isinstance(data, list):
return [self.normalize_keys(item, repl) for item in data]
else:
return data
def collect_statistics(self):
"""Сбор статистики по элементам, гибам и сваркам в активном документе"""
logger.info("Начинаем сбор статистики по элементам...")
av_actions = self.get_available_actions()
allowed_types = av_actions[KompasCommand.STATS]["allowed_types"]
docs_collection = self.application.Documents
result = {"result": []}
for i in range(docs_collection.Count + 1):
try:
doc = docs_collection.Item(i)
if doc is None:
continue
doc_type = doc.DocumentType
if doc_type not in allowed_types:
continue
doc.Active = True
doc_name = "-".join(doc.Name.split(".")[:-1])
logger.info(f"Обрабатываем документ: {doc_name}")
doc_3d = self.api7_module.IKompasDocument3D(doc)
top_part = doc_3d.TopPart
res = self.traverse_ipart(top_part)
# Формируем объект документа для результата
result["result"] = self.normalize_keys(res)
print(result["result"])
except Exception as e:
logger.error(f"[ERROR] Ошибка при обработке документа #{i + 1}: {e}")
traceback.print_exc()
return result
def export_to_raster(self):
"""Экспорт открытых 2D-документов (чертежи, фрагменты, спецификации) в JPG и DXF. Создание многостраничного PDF."""
logger.info("Начинаем экспорт документов в растровый формат...")
images = []
first_doc_name = None
result = {"result": []}
# Получаем доступные типы для экспорта из разрешенных действий
av_actions = self.get_available_actions()
allowed_types = av_actions[KompasCommand.EXPORT_RASTER]["allowed_types"]
docs_collection = self.application.Documents
for i in range(docs_collection.Count):
try:
doc = docs_collection.Item(i)
if doc is None:
continue
doc_type = doc.DocumentType
if doc_type not in allowed_types:
continue
doc.Active = True
doc_path = doc.Path
doc_name = os.path.splitext(doc.Name)[0] # Имя без расширения
logger.info(f"Обрабатываем документ: {doc_name}")
# Сохраняем имя первого документа
if first_doc_name is None:
try:
doc7 = self.api7_module.IKompasDocument(doc)
first_doc_name = (
doc7.LayoutSheets.ItemByNumber(1).Stamp.Text(2).Str
)
except Exception as e:
first_doc_name = "Без имени"
logger.warning("Не удалось получить имя документа из штампа")
# Получаем интерфейс 2D-документа
if doc_type == self.constants.ksDocumentSpecification:
doc_api5 = self.api5.SpcActiveDocument()
else:
doc_api5 = self.api5.ActiveDocument2D()
if not doc_api5:
raise RuntimeError("Не удалось получить активный 2D-документ")
# Параметры экспорта в JPG
raster_params = doc_api5.RasterFormatParam()
raster_params.Init()
raster_params.colorBPP = 8
raster_params.colorType = 3
raster_params.extResolution = 96
raster_params.format = 0 # JPEG format
paths = {}
# Сохранение в JPG и DXF
for ext in ["jpg", "dxf"]:
full_path = self.prepare_export_path(doc_path, doc_name, ext)
if ext == "jpg":
doc_api5.SaveAsToRasterFormat(full_path, raster_params)
logger.info(f"[OK] Сохранен JPG: {full_path}")
try:
img = Image.open(full_path)
images.append(img.copy())
img.close()
except Exception as e:
logger.error(
f"Не удалось открыть изображение: {full_path}",
exc_info=True,
)
elif ext == "dxf":
doc_api5.ksSaveToDXF(full_path)
logger.info(f"Сохранен DXF: {full_path}")
paths[ext] = full_path
# Добавляем результат в список
result["result"].append(
{
"document_name": doc_name,
"document_type": doc_type,
"paths": paths,
"success": True,
"timestamp": time.time(),
}
)
except Exception as e:
logger.error(f"[ERROR] Не удалось создать PDF: {e}", exc_info=True)
result["result"].append(
{
"file": None,
"success": False,
"error": str(e),
"document_index": i + 1,
"timestamp": time.time(),
}
)
# Создание PDF
if images:
desktop_path = os.path.expanduser("~\\Desktop")
pdf_filename = f"{first_doc_name}_pages.pdf"
pdf_output_path = os.path.join(desktop_path, pdf_filename)
# Создаем титульную страницу с названием и количеством страниц
try:
font = ImageFont.truetype("arial.ttf", size=48)
except IOError:
print("Шрифт Arial не найден. Используется стандартный шрифт.")
font = ImageFont.load_default()
title_image = Image.new("RGB", (images[0].width, 200), color="white")
draw = ImageDraw.Draw(title_image)
title_text = f"{first_doc_name}\nКоличество страниц: {len(images)}"
draw.text((10, 50), title_text, fill="black", font=font, spacing=10)
images.insert(0, title_image)
# Сохраняем как PDF
try:
images[0].save(
pdf_output_path,
save_all=True,
append_images=images[1:],
format="PDF",
resolution=96.0,
)
logger.info(f"[OK] PDF успешно сохранен: {pdf_output_path}")
result["result"].append(
{
"document_name": pdf_output_path,
"document_type": "pdf",
"paths": pdf_output_path,
"success": True,
"timestamp": time.time(),
}
)
except Exception as e:
logger.info(f"[ERROR] Не удалось создать PDF: {e}")
return result
def get_available_actions(self):
"""
Возвращает список доступных действий и допустимые типы документов
Формат: {
action_key: {
'label': str,
'allowed_types': list,
'method': str,
'command': str
}
}
"""
# Допустимые типы документов
ALLOWED_TYPES_3D = [
self.constants.ksDocumentPart,
self.constants.ksDocumentAssembly,
]
ALLOWED_TYPES_2D = [
self.constants.ksDocumentDrawing,
self.constants.ksDocumentFragment,
]
ALLOWED_TYPES_SPEC = [self.constants.ksDocumentSpecification]
ALLOWED_TYPES_ALL = ALLOWED_TYPES_3D + ALLOWED_TYPES_2D + ALLOWED_TYPES_SPEC
return {
KompasCommand.IGES: {
"label": "Сохранить как IGES",
"allowed_types": ALLOWED_TYPES_3D,
"method": "save_to_iges",
"command": KompasCommand.IGES,
},
KompasCommand.STATS: {
"label": "Собрать статистику по элементам",
"allowed_types": ALLOWED_TYPES_3D,
"method": "collect_statistics",
"command": KompasCommand.STATS,
},
KompasCommand.EXPORT_RASTER: {
"label": "Экспортировать в PDF",
"allowed_types": ALLOWED_TYPES_2D + ALLOWED_TYPES_SPEC,
"method": "export_to_raster",
"command": KompasCommand.EXPORT_RASTER,
},
KompasCommand.PROJECT_SUPPORT: {
"label": "Создать чертежи деталей",
"allowed_types": ALLOWED_TYPES_3D,
"method": "create_drawing_for_parts",
"command": KompasCommand.PROJECT_SUPPORT,
},
}