import os
import pandas as pd
from PyQt6.QtWidgets import (
QApplication,
QMainWindow,
QVBoxLayout,
QHBoxLayout,
QWidget,
QPushButton,
QListWidget,
QLabel,
QFileDialog,
QMessageBox,
QTextEdit,
)
from PyQt6.QtCore import QThread, pyqtSignal
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.utils import ImageReader
import logging

Настройка логирования
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)

def register_fonts():
try:
pdfmetrics.registerFont(TTFont('DejaVuSans', 'DejaVuSans.ttf'))
pdfmetrics.registerFont(TTFont('DejaVuSans-Bold', 'DejaVuSans-Bold.ttf'))
except Exception as e:
logging.error(f"Ошибка при регистрации шрифтов: {e}")
raise

register_fonts()

Задаем размер страницы 297 мм x 420 мм (формат A3)
custom_page_size = (297 * mm, 420 * mm)

class PDFGeneratorThread(QThread):
log_signal = pyqtSignal(str)


def __init__(self, folder_paths, save_path, photo_files):
    super().__init__()
    self.folder_paths = folder_paths
    self.save_path = save_path
    self.photo_files = photo_files

def run(self):
    try:
        data = self.aggregate_data()
        if data.empty:
            self.log_signal.emit("Нет данных для обработки.")
            return
        photo_data = self.aggregate_photo_data()
        self.generate_pdfs(data, photo_data)
    except Exception as e:
        self.log_signal.emit(f"Ошибка при выполнении: {e}")

def aggregate_data(self):
    data = pd.DataFrame()
    for folder in self.folder_paths:
        if not os.path.isdir(folder):
            self.log_signal.emit(f"Предупреждение: {folder} — не существует.")
            continue
        for file in os.listdir(folder):
            if file.endswith(('.xlsx', '.xls')):
                file_path = os.path.join(folder, file)
                try:
                    xls = pd.ExcelFile(file_path)
                    for sheet_name in xls.sheet_names:
                        file_data = pd.read_excel(file_path, sheet_name=sheet_name)
                        # Нормализация названий столбцов
                        file_data.columns = file_data.columns.str.strip()
                        if not file_data.empty:
                            self.log_signal.emit(
                                f"Прочитан файл: {file_path}, лист: {sheet_name}, столбцы: {file_data.columns.tolist()}"
                            )
                            data = pd.concat([data, file_data], ignore_index=True)
                        else:
                            self.log_signal.emit(f"Предупреждение: Лист {sheet_name} в файле {file_path} пустой.")
                except Exception as e:
                    self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
    # Нормализация названий столбцов в основном DataFrame
    if not data.empty:
        data.columns = data.columns.str.strip()
    return self.check_columns(data)

def check_columns(self, data):
    # Преобразование дат в формат datetime, если есть соответствующие столбцы
    if 'Период с' in data.columns:
        data["Период с"] = pd.to_datetime(data["Период с"], dayfirst=True, errors='coerce')
    if 'Период по' in data.columns:
        data["Период по"] = pd.to_datetime(data["Период по"], dayfirst=True, errors='coerce')
    return data

def aggregate_photo_data(self):
    photo_data = pd.DataFrame()
    for file_path in self.photo_files:
        if not os.path.exists(file_path):
            self.log_signal.emit(f"Предупреждение: Файл {file_path} не существует.")
            continue
        try:
            file_data = pd.read_excel(file_path)
            # Нормализация названий столбцов
            file_data.columns = file_data.columns.str.strip()
            if not file_data.empty:
                self.log_signal.emit(
                    f"Прочитан файл с фотографиями: {file_path}, столбцы: {file_data.columns.tolist()}"
                )
                photo_data = pd.concat([photo_data, file_data], ignore_index=True)
            else:
                self.log_signal.emit(f"Предупреждение: Файл с фотографиями {file_path} пустой.")
        except Exception as e:
            self.log_signal.emit(f"Ошибка при чтении файла {file_path}: {e}")
    return photo_data

def generate_pdfs(self, data, photo_data):
    if "Код ТТ" in data.columns:
        data_by_tt = data.groupby("Код ТТ")
    else:
        self.log_signal.emit("Отсутствует столбец 'Код ТТ' в данных. Невозможно сгенерировать PDF.")
        return
    for tt_code, tt_data in data_by_tt:
        if tt_data.empty:
            continue
        if "Название ТТ" in tt_data.columns:
            tt_name_series = tt_data["Название ТТ"].dropna()
            if not tt_name_series.empty:
                tt_name = tt_name_series.iloc[0]
            else:
                tt_name = "Unknown TT Name"
        else:
            tt_name = "Unknown TT Name"

        period_start = tt_data["Период с"].min() if "Период с" in tt_data.columns else None
        period_end = tt_data["Период по"].max() if "Период по" in tt_data.columns else None

        period_start_str = period_start.strftime('%Y-%m-%d') if pd.notnull(period_start) else "unknown_start"
        period_end_str = period_end.strftime('%Y-%m-%d') if pd.notnull(period_end) else "unknown_end"

        safe_tt_name = "".join([c if c.isalnum() or c in (' ', '_', '-') else "_" for c in tt_name])
        pdf_name = f"{tt_code}_{safe_tt_name}_{period_start_str}_{period_end_str}.pdf"
        pdf_path = os.path.join(self.save_path, pdf_name)

        c = canvas.Canvas(pdf_path, pagesize=custom_page_size)
        width, height = custom_page_size

        y_position = height - 50

        self.draw_text(c, "Администратору необходимо осуществить выкладку товара на ДМП:", 30, y_position, "DejaVuSans", 10)
        y_position -= 20

        equipment_types = [
            "Магнит-хит", "Оборудование КА", "Навесное оборудование",
            "Оборудование сети", "Желтый акционный стеллаж"
        ]
        for equipment_type in equipment_types:
            if "Тип оборудования" in tt_data.columns:
                eq_data = tt_data[tt_data["Тип оборудования"] == equipment_type]
                if not eq_data.empty:
                    y_position = self.start_new_page_if_needed(c, y_position, height)
                    self.draw_text(c, f"Оборудование: {equipment_type}", 30, y_position, "DejaVuSans-Bold", 10)
                    y_position -= 20

                    y_position = self.process_projects(c, eq_data, equipment_type, photo_data, width, height, y_position)
        c.save()
        self.log_signal.emit(f"PDF создан: {pdf_path}")

def process_projects(self, c, eq_data, equipment_type, photo_data, width, height, y_position):
    left_margin = 30
    right_margin = 30
    column_gap = 10
    max_page_width = width - left_margin - right_margin
    left_column_width = max_page_width * 0.6
    right_column_width = max_page_width * 0.4

    for project, project_data in eq_data.groupby("Наименование проекта"):
        if project_data.empty:
            continue
        # Определяем высоту, необходимую для вывода деталей проекта
        font_size = 8
        detail_lines = self.get_details(equipment_type, project_data)
        detail_height = len(detail_lines) * (font_size + 10)  # приблизительная высота деталей
        project_title_height = font_size + 10  # высота строки "Проект:..."

        # Проверяем, хватит ли места на текущей странице для деталей проекта
        required_height = project_title_height + detail_height + 50  # +50 для запаса
        if y_position - required_height < 100:
            # Места не хватает, переносим весь проект на новую страницу
            c.showPage()
            y_position = height - 50

        # Начинаем с левого столбца
        current_x = left_margin

        project_title_y = y_position  # Запоминаем y_position для верхней части фото

        # Используем новый метод draw_wrapped_text
        max_text_width = left_column_width - 20  # Отнимаем отступы
        self.draw_wrapped_text(c, f"Проект: {project}", current_x, y_position, "DejaVuSans", font_size, max_text_width)
        y_position -= 20

        details = detail_lines
        for detail in details:
            y_position -= 18
            self.draw_wrapped_text(c, detail, current_x + 10, y_position, "DejaVuSans", font_size, max_text_width - 10)
        y_position -= 10  # Отступ после деталей

        # Сохраняем y_position перед выводом таблицы
        y_position_before_table = y_position

        table_columns = self.get_table_columns(equipment_type, project_data)
        if table_columns:
            table_data = project_data[table_columns].drop_duplicates().fillna("-")
            y_position_after_table = self.draw_table(c, table_data, table_columns, left_column_width, y_position, current_x)
        else:
            y_position_after_table = y_position
            self.log_signal.emit(f"Табличные данные отсутствуют или необходимые столбцы отсутствуют для проекта '{project}'.")

        # Теперь добавляем изображения после вывода таблицы
        # y_top остается прежним, y_bottom обновляется до y_position после таблицы
        y_bottom = y_position_after_table

        self.add_photos_to_pdf(
            c, photo_data, project, right_column_width, project_title_y, y_bottom,
            left_margin + left_column_width + column_gap
        )

        y_position = y_position_after_table - 20  # Отступ перед следующим проектом

    return y_position

def start_new_page_if_needed(self, c, y_position, height):
    if y_position < 100:
        c.showPage()
        y_position = height - 50
    return y_position

def get_details(self, equipment_type, project_data):
    details = []

    def add_detail(label, column_name):
        if column_name in project_data.columns:
            value_series = project_data[column_name].dropna()
            if not value_series.empty:
                value = value_series.iloc[0]
            else:
                value = '-'
            details.append(f"{label}: {value}")
        else:
            details.append(f"{label}: -")

    if equipment_type in ["Магнит-хит", "Оборудование сети"]:
        add_detail("Название оборудования", "Наименование оборудования")
        add_detail("Полный период размещения", "Полный период размещения")
        add_detail("Ссылка на планограмму", "Ссылка на ПГ")
        add_detail("Комментарий", "Комментарий")
    elif equipment_type in ["Навесное оборудование", "Оборудование КА"]:
        add_detail("Зона размещения", "Зона размещения")
        add_detail("Название оборудования", "Наименование оборудования")
        add_detail("Полный период размещения", "Полный период размещения")
        add_detail("Действия ТТ по окончанию размещения", "Действия ТТ по окончанию размещения")
        add_detail("Комментарий", "Комментарий")
    elif equipment_type == "Желтый акционный стеллаж":
        add_detail("Зона размещения", "Зона размещения")
        add_detail("Ссылка на планограмму", "Ссылка на ПГ")
        add_detail("Комментарий", "Комментарий")
    return details

def get_table_columns(self, equipment_type, project_data):
    base_columns = []
    if equipment_type in ["Магнит-хит", "Оборудование сети", "Желтый акционный стеллаж"]:
        base_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт", "Место выкладки товара (адрес)"]
    elif equipment_type in ["Навесное оборудование", "Оборудование КА"]:
        base_columns = ["Код позиции", "Наименование позиции", "Загрузка в шт"]

    # Проверяем наличие столбцов в данных проекта
    table_columns = [col for col in base_columns if col in project_data.columns]
    missing_columns = [col for col in base_columns if col not in project_data.columns]
    if missing_columns:
        self.log_signal.emit(f"Предупреждение: В проекте отсутствуют столбцы {missing_columns}.")
    return table_columns

def draw_table(self, c, table_data, table_columns, max_table_width, y_position, x_start):
    if table_data.empty:
        self.log_signal.emit(f"Табличные данные отсутствуют.")
        return y_position

    self.draw_text(c, "Товарные позиции:", x_start + 10, y_position, "DejaVuSans", 8)
    y_position -= 20

    font_size = 8  # Размер шрифта 8
    col_widths = [self.calculate_column_width(table_data[col], col, font_size=font_size) for col in table_columns]
    total_width = sum(col_widths)
    scaling_factor = 1.0
    if total_width > max_table_width - 20:
        scaling_factor = (max_table_width - 20) / total_width
        col_widths = [w * scaling_factor for w in col_widths]

    current_x = x_start + 10
    c.setFont("DejaVuSans-Bold", font_size)
    row_height = font_size + 6  # Высота строки таблицы
    c.setLineWidth(0.5)
    height = c._pagesize[1]

    # Заголовок таблицы
    for i, col in enumerate(table_columns):
        c.drawString(current_x + 2, y_position, col)
        c.rect(current_x, y_position - 2, col_widths[i], row_height)
        current_x += col_widths[i]
    y_position -= row_height

    c.setFont("DejaVuSans", font_size)
    for index, row in table_data.iterrows():
        if y_position < 100:
            c.showPage()
            y_position = height - 50
            # Рисуем заголовок таблицы на новой странице
            current_x = x_start + 10
            c.setFont("DejaVuSans-Bold", font_size)
            for i, col in enumerate(table_columns):
                c.drawString(current_x + 2, y_position, col)
                c.rect(current_x, y_position - 2, col_widths[i], row_height)
                current_x += col_widths[i]
            y_position -= row_height
            c.setFont("DejaVuSans", font_size)

        current_x = x_start + 10
        for i, col in enumerate(table_columns):
            c.drawString(current_x + 2, y_position, str(row[col]))
            c.rect(current_x, y_position - 2, col_widths[i], row_height)
            current_x += col_widths[i]
        y_position -= row_height

    return y_position

def add_photos_to_pdf(self, c, photo_data, project, photo_zone_width, y_top, y_bottom, x_start):
    if 'Название проекта' in photo_data.columns:
        photo_rows = photo_data.loc[photo_data['Название проекта'] == project]
        if not photo_rows.empty:
            for _, photo_row in photo_rows.iterrows():
                if 'Путь' in photo_row:
                    photo_path = photo_row['Путь']
                    if os.path.exists(photo_path):
                        try:
                            img = ImageReader(photo_path)
                            img_width, img_height = img.getSize()

                            # Доступная высота для изображения
                            available_height = y_top - y_bottom - 10  # 10 - небольшой отступ
                            # Доступная ширина
                            available_width = photo_zone_width - 20

                            # Проверяем, что доступная высота положительная
                            if available_height <= 0 or available_width <= 0:
                                self.log_signal.emit(
                                    f"Предупреждение: недостаточно места для размещения изображения для проекта '{project}'."
                                )
                                continue

                            # Масштабируем изображение пропорционально, чтобы оно поместилось в доступное пространство
                            scale_factor = min(available_width / img_width, available_height / img_height)
                            display_width = img_width * scale_factor
                            display_height = img_height * scale_factor

                            # Позиция изображения по вертикали
                            y_position = y_top - display_height

                            c.drawImage(
                                img,
                                x_start + 10,
                                y_position,
                                width=display_width,
                                height=display_height,
                                preserveAspectRatio=True,
                                mask='auto'
                            )
                            # Изображение не должно перекрывать таблицу и текст
                            break
                        except Exception as img_e:
                            self.log_signal.emit(f"Ошибка при добавлении фотографии {photo_path}: {img_e}")
                    else:
                        self.log_signal.emit(f"Фотография для проекта {project} не найдена: {photo_path}")
                else:
                    self.log_signal.emit(f"Предупреждение: В данных фотографий отсутствует столбец 'Путь'.")
        else:
            self.log_signal.emit(f"Фотографии для проекта '{project}' не найдены.")
    else:
        self.log_signal.emit("Предупреждение: В данных фотографий отсутствует столбец 'Название проекта'.")

def calculate_column_width(self, series, col_name, font_name='DejaVuSans', font_size=8):
    max_width_data = series.astype(str).apply(lambda x: self.string_width(x, font_name, font_size)).max()
    col_header_width = self.string_width(col_name, "DejaVuSans-Bold", font_size)
    return max(max_width_data, col_header_width) + 10

def string_width(self, text, font_name, font_size):
    return pdfmetrics.stringWidth(text, font_name, font_size)

def draw_text(self, canvas_obj, text, x, y, font_name, font_size):
    canvas_obj.setFont(font_name, font_size)
    canvas_obj.drawString(x, y, text)

# Новый метод для рисования текста с переносом строк
def draw_wrapped_text(self, canvas_obj, text, x, y, font_name, font_size, max_width):
    canvas_obj.setFont(font_name, font_size)
    words = text.split(' ')
    lines = []
    current_line = ''
    for word in words:
        test_line = current_line + ' ' + word if current_line else word
        if self.string_width(test_line, font_name, font_size) <= max_width:
            current_line = test_line
        else:
            lines.append(current_line)
            current_line = word
    if current_line:
        lines.append(current_line)
    for line in lines:
        canvas_obj.drawString(x, y, line)
        y -= font_size + 4  # Задаем интервал между строками
    return y
class PDFGeneratorApp(QMainWindow):
def init(self):
super().init()
self.setWindowTitle("Конвертер Excel в PDF")
self.folder_paths = []
self.save_path = ""
self.photo_files = []


    main_widget = QWidget()
    self.setCentralWidget(main_widget)

    main_layout = QVBoxLayout()
    main_widget.setLayout(main_layout)

    self.folder_listbox = QListWidget()
    main_layout.addWidget(self.folder_listbox)

    folder_buttons_layout = QHBoxLayout()
    main_layout.addLayout(folder_buttons_layout)

    self.add_folder_button = QPushButton("Добавить папку")
    self.add_folder_button.clicked.connect(self.add_folder)
    folder_buttons_layout.addWidget(self.add_folder_button)

    self.remove_folder_button = QPushButton("Удалить папку")
    self.remove_folder_button.clicked.connect(self.remove_folder)
    folder_buttons_layout.addWidget(self.remove_folder_button)

    self.save_path_button = QPushButton("Выбрать путь для сохранения PDF")
    self.save_path_button.clicked.connect(self.choose_save_path)
    main_layout.addWidget(self.save_path_button)

    self.save_path_label = QLabel("Путь для сохранения PDF не выбран")
    main_layout.addWidget(self.save_path_label)

    self.photo_files_button = QPushButton("Выбрать файлы с фотографиями")
    self.photo_files_button.clicked.connect(self.choose_photo_files)
    main_layout.addWidget(self.photo_files_button)

    self.photo_files_label = QLabel("Файлы с фотографиями не выбраны")
    main_layout.addWidget(self.photo_files_label)

    self.start_button = QPushButton("Запуск")
    self.start_button.clicked.connect(self.start_processing)
    main_layout.addWidget(self.start_button)

    self.close_button = QPushButton("Закрыть")
    self.close_button.clicked.connect(self.close)
    main_layout.addWidget(self.close_button)

    self.log_text_edit = QTextEdit()
    self.log_text_edit.setReadOnly(True)
    main_layout.addWidget(self.log_text_edit)

    self.apply_styles()

def apply_styles(self):
    stylesheet = """
    QMainWindow {
        background-color: #f0f0f0;
    }
    QPushButton {
        background-color: #4CAF50;
        color: white;
        padding: 10px 20px;
        border: none;
        border-radius: 5px;
        font-size: 14px;
        margin: 5px;
    }
    QPushButton:hover {
        background-color: #45a049;
    }
    QListWidget {
        background-color: white;
        border: 1px solid #ddd;
        border-radius: 5px;
        padding: 10px;
        font-size: 14px;
    }
    QLabel {
        font-size: 14px;
        color: #333;
    }
    QTextEdit {
        background-color: white;
        border: 1px solid #ddd;
        border-radius: 5px;
        padding: 10px;
        font-size: 12px;
    }
    """
    self.setStyleSheet(stylesheet)

def add_folder(self):
    folder_path = QFileDialog.getExistingDirectory(self, "Выберите папку")
    if folder_path:
        if folder_path not in self.folder_paths:
            self.folder_paths.append(folder_path)
            self.folder_listbox.addItem(folder_path)
            self.log_info(f"Добавлена папка: {folder_path}")
        else:
            QMessageBox.information(self, "Информация", "Эта папка уже добавлена.")
            self.log_info(f"Попытка добавить уже существующую папку: {folder_path}")

def remove_folder(self):
    selected_items = self.folder_listbox.selectedItems()
    if not selected_items:
        QMessageBox.information(self, "Информация", "Пожалуйста, выберите папку для удаления.")
        return
    for item in selected_items:
        self.folder_paths.remove(item.text())
        self.folder_listbox.takeItem(self.folder_listbox.row(item))
        self.log_info(f"Удалена папка: {item.text()}")

def choose_save_path(self):
    save_path = QFileDialog.getExistingDirectory(self, "Выберите путь для сохранения PDF")
    if save_path:
        self.save_path = save_path
        self.save_path_label.setText(f"Сохранение в: {self.save_path}")
        self.log_info(f"Выбран путь для сохранения PDF: {self.save_path}")

def choose_photo_files(self):
    file_paths, _ = QFileDialog.getOpenFileNames(
        self, "Выберите файлы с фотографиями", "", "Excel files (*.xlsx *.xls)"
    )
    if file_paths:
        self.photo_files = file_paths
        display_paths = ', '.join([os.path.basename(path) for path in file_paths])
        self.photo_files_label.setText(f"Выбраны файлы: {display_paths}")
        self.log_info(f"Выбраны файлы с фотографиями: {display_paths}")

def start_processing(self):
    if not self.folder_paths:
        QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите хотя бы одну папку с файлами Excel.")
        self.log_error("Ошибка: не выбрана ни одна папка с файлами Excel.")
        return
    if not self.save_path:
        QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите путь для сохранения PDF файлов.")
        self.log_error("Ошибка: не выбран путь для сохранения PDF файлов.")
        return
    if not self.photo_files:
        QMessageBox.warning(self, "Ошибка", "Пожалуйста, выберите файлы с фотографиями.")
        self.log_error("Ошибка: не выбраны файлы с фотографиями.")
        return

    self.start_button.setEnabled(False)
    self.pdf_thread = PDFGeneratorThread(self.folder_paths, self.save_path, self.photo_files)
    self.pdf_thread.log_signal.connect(self.log_info)
    self.pdf_thread.finished.connect(self.processing_finished)
    self.pdf_thread.start()
    self.log_info("Начато выполнение процесса генерации PDF.")

def processing_finished(self):
    self.start_button.setEnabled(True)
    self.log_info("Процесс генерации PDF завершен.")

def log_info(self, message):
    self.log_text_edit.append(f"INFO: {message}")
    logging.info(message)

def log_error(self, message):
    self.log_text_edit.append(f"ERROR: {message}")
    logging.error(message)
if name == "main":
app = QApplication([])
window = PDFGeneratorApp()
window.show()
app.exec()
Необходимо скорректировать код. В логах по некоторым проектам выводится информация, что не удалось добавить изображение, так как не хватил места. Нужно поправить логику размещения изображения для таких проектов. Нужно уменьшить изображение чтобы верхний предел изображения находился на одном уровне со строкой с названием проекта, нижний придел изображения до конца таблицы именно на этом листе. Можно попробовать представить это пространство как прямоугольник, который имеет границы сверху от строки с название проекта, снизу по краю листа, справа 10 мм до конца листа, слева 10 мм от таблицы. Фото необходимо уменьшить и разместить в этой прямоугольной зоне. Эту логику необходимо применять только для тех проектов, где изображение не помещается стандартным способом, для остальных все должно остаться так же. Перепиши весь этот код с учетом модификации.