commit 93b28b1ea24951f35d7aa7b5fe9d10c4087a9ecf Author: gkonoplya Date: Sat Feb 21 11:46:24 2026 +0300 Add initial project structure and core functionality for label extraction and PDF generation - Created .gitignore to exclude unnecessary files and directories. - Implemented build_pdf.py for generating PDF labels from Excel data, including barcode rendering. - Added read_image.py for extracting DataMatrix codes from images using zxing-cpp. - Introduced render_eps.py for converting EPS images to PNG format with Ghostscript. - Updated README.md with project overview, features, installation instructions, and usage guidelines. - Included requirements.txt for dependency management. - Added resources for logos and sample Excel template. - Compressed Ghostscript binaries into Ghostscript.zip for easy integration. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..108e0a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,166 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Data +data/ +Ghostscript/ + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache +flask_session/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# poetry +poetry.lock + +# pdm +.pdm.toml +.pdm-python + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ +.project +.pydevproject +.settings/ + +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db +Desktop.ini + +# Project-specific +*.bak +*.tmp diff --git a/Ghostscript.zip b/Ghostscript.zip new file mode 100644 index 0000000..50d11f0 Binary files /dev/null and b/Ghostscript.zip differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3dd939d --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# LabelExtractor (Честный ЗНАК / CRPT) + +Утилита для автоматизированного извлечения кодов маркировки (DataMatrix) и массовой генерации готовых к печати PDF-этикеток (формат 58x40 мм) на основе данных из Excel-шаблона. Решение оптимизировано для работы с системой «Честный ЗНАК» и создания этикеток для термопринтеров. + +## Ключевые возможности + +* **Распознавание DataMatrix:** Точное чтение кодов из изображений с помощью `zxing-cpp` с сохранением непечатаемых спецсимволов GS1 (включая FNC1 / ASCII 29) посредством кодирования в Base64. +* **Интеграция с Excel:** Парсинг атрибутов товара (GTIN, EAN, Описание, Артикул, Цвет, Размер, Организация) из Excel-шаблонов с использованием `pandas`. Чтение строго по позициям колонок обеспечивает защиту от изменения заголовков. +* **PDF Генерация:** Автоматическая сборка промышленных макетов этикеток (включая логотипы EAC, Честный ЗНАК, штрихкоды Code128 и DataMatrix) с помощью `reportlab`. +* **Умное форматирование текста:** Автоматический перенос строк (Word Wrap), адаптивное уменьшение размера шрифта и умное усечение текста многоточием (при выходе за границы AABB). +* **Портативность:** Использование встроенной версии Ghostscript (`gswin32c`) для рендеринга EPS и штрихкодов без необходимости сложной системной настройки. + +## Требования и установка + +Проект написан на Python 3. Установите необходимые зависимости из `requirements.txt`: + +```bash +pip install -r requirements.txt +``` + +**Зависимости:** +* `pillow` — работа с изображениями. +* `zxing-cpp` — быстрое и надежное чтение штрихкодов. +* `reportlab` — генерация векторных PDF-файлов. +* `pandas` & `openpyxl` — чтение и обработка Excel-таблиц. +* `treepoem` — генерация штрихкодов (Code128, DataMatrix). + +*Примечание: Убедитесь, что архив `Ghostscript.zip` распакован в папку `Ghostscript` в корне проекта для корректной работы `treepoem` и обработки EPS-файлов.* + +## Структура проекта + +* `build_pdf.py` — Главный модуль и оркестратор бизнес-логики. Считывает базу данных Excel, группирует входные коды по GTIN, рендерит штрихкоды и генерирует готовые PDF-документы. +* `read_image.py` — Утилита для извлечения байтов DataMatrix из картинок и перевода их в безопасный Base64 формат. +* `render_eps.py` — Скрипт-конвертер EPS изображений в PNG с использованием портативного Ghostscript. +* `Resources/` — Папка с графическими ассетами (логотипы) и исходным файлом `ШАблон для загрузки этикеток.xlsx`. +* `data/` — Рабочая директория (содержит входные коды, картинки и генерируемые PDF в папке `output_pdfs`). + +## Использование + +1. Подготовьте описания ваших товаров в файле `Resources/ШАблон для загрузки этикеток.xlsx`. +2. Подготовьте список КМ (кодов маркировки), предварительно закодировав сырые байты DataMatrix в формат Base64. (Для получения Base64 из изображений можно использовать скрипт `read_image.py`). +3. Запустите процесс генерации PDF: + +```bash +python build_pdf.py +``` + +Готовые многостраничные PDF-файлы (сгруппированные по GTIN) будут сохранены в директории `data/output_pdfs/`. + +## Архитектура решения + +Код разбит на несколько логических слоев: +1. **Domain Layer:** Описание структур данных (`LabelData`). +2. **Data & Asset Layer:** Функции генерации и трансформации изображений и штрихкодов (`render_code128`, `create_datamatrix_in_memory`). Изображения генерируются без сглаживания (`NEAREST`) для идеальной печати на термопринтерах. +3. **Presentation Layer:** Низкоуровневая отрисовка PDF-холста через `reportlab`, позиционирование блоков и алгоритмы подгонки текста (`draw_label_page`, `place_text`). +4. **Business Logic Layer:** Оркестрация батчевой обработки данных (`process_batch`). Чтение тяжелых файлов происходит один раз для экономии ресурсов I/O диска. diff --git a/build_pdf.py b/build_pdf.py new file mode 100644 index 0000000..03085ad --- /dev/null +++ b/build_pdf.py @@ -0,0 +1,418 @@ +import os +import base64 +from collections import defaultdict +from dataclasses import dataclass +import pandas as pd +from PIL import Image + +from reportlab.pdfgen import canvas +from reportlab.lib.units import mm +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.lib.utils import ImageReader +from reportlab.platypus import Paragraph +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT + +from read_image import read_datamatrix_zxing +import render_eps +import treepoem + +# --------------------------------------------------------- +# 1. СТРУКТУРЫ ДАННЫХ (DOMAIN LAYER) +# --------------------------------------------------------- + +@dataclass +class LabelData: + gtin: str + ean: str + description: str + article: str + color: str + size: str + organization: str + page_num: int = 0 + + @classmethod + def from_excel_row(cls, row: pd.Series): + """ + Парсит строку DataFrame по строго заданным ПОЗИЦИЯМ колонок (индексам). + Игнорирует названия заголовков в Excel-файле для защиты от их случайного изменения. + + Ожидаемый порядок колонок: + 0: GTIN (14) + 1: EAN (13) + 2: Описание (200) + 3: Артикул (20) + 4: Цвет (20) + 5: Размер (15) + 6: Организация (100) + """ + def extract_by_index_and_limit(col_index: int, limit: int) -> str: + # Безопасное извлечение по индексу: если колонок меньше, чем мы ожидаем, вернем пустоту + if col_index >= len(row): + return '' + + val = str(row.iloc[col_index]) + + # Очистка от мусора парсера pandas + if val.lower() == 'nan' or not val.strip(): + return '' + + return val[:limit] + + return cls( + gtin=extract_by_index_and_limit(0, 14), # Колонка 1 (индекс 0) + ean=extract_by_index_and_limit(1, 13), # Колонка 2 (индекс 1) + description=extract_by_index_and_limit(2, 200), # Колонка 3 (индекс 2) + article=extract_by_index_and_limit(3, 20), # Колонка 4 (индекс 3) + color=extract_by_index_and_limit(4, 20), # Колонка 5 (индекс 4) + size=extract_by_index_and_limit(5, 15), # Колонка 6 (индекс 5) + organization=extract_by_index_and_limit(6, 100) # Колонка 7 (индекс 6) + ) + + +# --------------------------------------------------------- +# 2. РАБОТА С ДАННЫМИ И ИЗОБРАЖЕНИЯМИ (DATA & ASSET LAYER) +# --------------------------------------------------------- + +def load_png(file_path: str): + img = Image.open(file_path) + img = img.convert("RGBA") + img = img.resize((img.width * 5, img.height * 5), Image.Resampling.NEAREST) + return ImageReader(img) + +def render_code128(text: str) -> ImageReader: + """Генерирует Code128 штрихкод через Treepoem и Ghostscript (без новых зависимостей).""" + # Защита от пустых или невалидных строк, чтобы предотвратить сбой Ghostscript + if not text or str(text).lower() == 'nan': + text = "0000000000000" + + img = treepoem.generate_barcode( + barcode_type='code128', + data=str(text) + ) + + # Очищаем картинку для термопринтера (масштабируем без сглаживания) + img = img.resize((img.width * 5, img.height * 5), Image.Resampling.NEAREST) + + return ImageReader(img) + +def extract_key_from_base64(b64_data: str) -> tuple[bytes, str]: + """Возвращает сырые байты (для рендера) и 14-значный ключ (GTIN) для группировки/поиска.""" + raw_bytes = base64.b64decode(b64_data) + raw_string = raw_bytes.decode('utf-8', errors='ignore') + # Срез с 3 по 16-й символ, как вы описывали ранее + search_key = raw_string[2:16] + return raw_bytes, search_key + + +def create_datamatrix_in_memory(raw_bytes: bytes) -> ImageReader: + """Генерирует DataMatrix через Treepoem и Ghostscript (без Си-зависимостей).""" + + # treepoem напрямую и безопасно передает сырые байты (raw_bytes) в Ghostscript, + # сохраняя все непечатаемые спецсимволы GS1 (ASCII 29) + img = treepoem.generate_barcode( + barcode_type='datamatrix', + data=raw_bytes + ) + + # Очищаем картинку для термопринтера (убираем сглаживание) + img = img.resize((img.width * 10, img.height * 10), Image.Resampling.NEAREST) + + return ImageReader(img) + +# --------------------------------------------------------- +# 3. РЕНДЕРИНГ PDF (PRESENTATION LAYER) +# --------------------------------------------------------- + +def init_pdf_font(): + """Регистрирует системный шрифт с поддержкой кириллицы (Regular и Bold).""" + font_name = "Arial" + + # Обычный шрифт + if os.path.exists("C:\\Windows\\Fonts\\arial.ttf"): + pdfmetrics.registerFont(TTFont('Arial', 'C:\\Windows\\Fonts\\arial.ttf')) + else: + pdfmetrics.registerFont(TTFont('Arial', 'Helvetica')) + + # Жирный шрифт + if os.path.exists("C:\\Windows\\Fonts\\arialbd.ttf"): + pdfmetrics.registerFont(TTFont('Arial-Bold', 'C:\\Windows\\Fonts\\arialbd.ttf')) + else: + pdfmetrics.registerFont(TTFont('Arial-Bold', 'Helvetica-Bold')) + + return font_name + + +def place_text(c: canvas.Canvas, text: str, font_name: str, + x: float, y: float, width: float, height: float, + font_size: float, font_min = 3, auto_resize = False, font_type: str = 'regular', align: str = 'left'): + """ + Размещает текст внутри AABB с автоматическим переносом строк (Word Wrap). + Если текст не помещается по высоте в заданный AABB, он автоматически обрезается + с добавлением многоточия '...'. + """ + if not text or str(text).lower() == 'nan' or not str(text).strip(): + return + + current_font = f"{font_name}-Bold" if font_type == 'bold' else font_name + + # Возвращаем РОДНЫЕ константы ReportLab + if align == 'center': + reportlab_align = TA_CENTER + elif align == 'right': + reportlab_align = TA_RIGHT + else: + reportlab_align = TA_LEFT + + # Создаем стиль для абзаца + style = ParagraphStyle( + name='CustomStyle', + fontName=current_font, + fontSize=font_size, + leading=font_size , + alignment=reportlab_align, # type: ignore + ) + + # 1. Алгоритм усечения текста (Truncation) + original_text = str(text) + current_text = original_text + + while True: + # Заменяем явные переносы на HTML-тег для ReportLab + text_for_pdf = current_text.replace('\n', '
') + p = Paragraph(text_for_pdf, style) + + # Вычисляем реальные размеры + actual_width, actual_height = p.wrap(width, height) + + # Если текст поместился по высоте, или от него осталась только одна буква + if actual_height <= height or len(current_text) <= 3: + break + + #Если не помещается, стараемся сначала уменьшить размер шрифта + if style.fontSize > font_min and auto_resize: + style.fontSize -= 0.1 + style.leading = style.fontSize + continue + + # Если не поместился, откусываем часть с конца и добавляем многоточие + # Откусываем по 3 символа за итерацию для скорости + if current_text.endswith('...'): + current_text = current_text[:-6] + '...' + else: + current_text = current_text[:-3] + '...' + + # 2. Вертикальное выравнивание + # Размещаем параграф так, чтобы он был отцентрирован по вертикали внутри бокса + y_draw = y + (height - actual_height) / 2.0 + + # Запасная защита (если даже один символ не влезает в бокс по высоте) + if actual_height > height: + y_draw = y + height - actual_height + + # 3. Отрисовка + p.drawOn(c, x, y_draw) + + # ========================================================= + # ЛАЙФХАК ДЛЯ ОТЛАДКИ (можете закомментировать после настройки) + # ========================================================= + #c.setStrokeColorRGB(0.8, 0.8, 0.8) # Светло-серая линия + #c.rect(x, y, width, height) + + +def draw_label_page(c: canvas.Canvas, font_name: str, dm_image: ImageReader, label_data: LabelData, readable_km: str): + """Отрисовывает ОДНУ страницу по промышленному макету (левая и правая зоны).""" + label_width = 58 * mm + label_height = 40 * mm + + # ========================================================= + # 1. ЛЕВАЯ ПАНЕЛЬ (DataMatrix + КМ) + # ========================================================= + # DataMatrix (крупный квадрат слева сверху) + img_size = 23 * mm + img_x = 2 * mm + img_y = label_height - img_size - 2 * mm # Верхний левый угол + + c.drawImage(dm_image, img_x, img_y, width=img_size, height=img_size) + + # Код Маркировки в 2 строки + # Если мы ранее добавили пробел в КМ, Paragraph сам перенесет его. + # Но для надежности мы принудительно заменяем пробел на тег новой строки: + km_two_lines = readable_km.replace(' ', '\n') + + km_box_h = 5 * mm + km_box_y = img_y - km_box_h + place_text(c, km_two_lines, font_name, + x=img_x, y=km_box_y, width=img_size, height=km_box_h, + font_size=5, font_min = 3, font_type='regular', align='center') + + CHZ_image_h = 5 * mm + CHZ_image_y = km_box_y - CHZ_image_h + CHZ_width_mm = 15 + #c.drawBoundary(c, img_x, CHZ_image_y, width=10 * mm, height=CHZ_image_h) + c.drawImage(load_png('resources/logo-CHZ-grey.png'), img_x, CHZ_image_y, width=CHZ_width_mm * mm, height=CHZ_image_h, mask='auto') + + eac_image_h = CHZ_image_h + eac_image_x = img_x + (CHZ_width_mm + 2) * mm + eac_image_y = CHZ_image_y + c.drawImage(load_png('resources/eac-conformity-mark-seeklogo.png'), eac_image_x, eac_image_y, width=eac_image_h, height=CHZ_image_h, mask='auto') + + page_num_h = 2 * mm + page_num_y = CHZ_image_y - page_num_h - 0.3 * mm + + place_text(c, f'№ {label_data.page_num}', font_name, + x=img_x, y=page_num_y, width=10 * mm, height=page_num_h, + font_size=8, font_min = 3, font_type='bold', align='center') + #c.drawBoundary(c, img_x, page_num_y, width=10 * mm, height=page_num_h) + + # Ниже на левой панели у вас останется место под логотипы ЧЗ, EAC и №1 + # ... (резерв места от y=0 до y=10мм) + + # ========================================================= + # 2. ПРАВАЯ ПАНЕЛЬ (Сборка AABB снизу вверх) + # ========================================================= + right_x = img_x + img_size + 2 * mm + right_w = label_width - right_x - 2 * mm # Около 29 мм + + # 7. EAN Текст (самый низ) + ean_text_h = 2.5 * mm + ean_text_y = 1 * mm + place_text(c, f"{label_data.ean}", font_name, + x=right_x, y=ean_text_y, width=right_w, height=ean_text_h, + font_size=6, font_type='regular', align='center', auto_resize=True) + + # 6. EAN Штрихкод (Серый прямоугольник-заглушка) + ean_barcode_h = 9 * mm + ean_barcode_y = ean_text_y + ean_text_h + code128_image = render_code128(label_data.ean) + c.drawImage(code128_image, right_x, ean_barcode_y, width=right_w, height=ean_barcode_h) + + ean_barcode_h += 0.5 + + # 5. Артикул (одна строка, жирно) + art_h = 2 * mm + art_y = ean_barcode_y + ean_barcode_h + place_text(c, f"арт.: {label_data.article}", font_name, + x=right_x, y=art_y, width=right_w, height=art_h, + font_size=5, font_type='bold', align='center') + + # 4. Размер (одна строка, жирно) + size_h = 2 * mm + size_y = art_y + art_h + place_text(c, f"размер: {label_data.size}", font_name, + x=right_x, y=size_y, width=right_w, height=size_h, + font_size=5, font_type='bold', align='center') + + # 3. Цвет (одна строка, жирно) + color_h = 2 * mm + color_y = size_y + size_h + place_text(c, f"цвет: {label_data.color}", font_name, + x=right_x, y=color_y, width=right_w, height=color_h, + font_size=5, font_type='bold', align='center') + + # 2. Описание (Крупно, жирно, центр, занимает всё среднее пространство) + desc_h = 16 * mm + desc_y = color_y + color_h + place_text(c, f"{label_data.description}", font_name, + x=right_x, y=desc_y, width=right_w, height=desc_h, + font_size=7, font_type='bold', align='center', auto_resize=True) + + # 1. Организация (На самом верху, мелко) + org_h = 3 * mm + org_y = desc_y + desc_h + place_text(c, f"{label_data.organization}", font_name, + x=right_x, y=org_y, width=right_w, height=org_h, + font_size=4.5, font_type='bold', align='center', auto_resize=True) + + c.showPage() + + +# --------------------------------------------------------- +# 4. ОРКЕСТРАТОР ПАЙПЛАЙНА (BUSINESS LOGIC) +# --------------------------------------------------------- + +def process_batch(base64_codes: list[str], excel_path: str, output_dir: str): + """ + Группирует КМ по GTIN, читает структуру из Excel один раз + и собирает многостраничные PDF документы. + """ + if not os.path.exists(output_dir): + os.makedirs(output_dir) + + # 1. Читаем базу данных (Excel) один раз, чтобы не дергать диск в цикле + print(f"Чтение Excel-шаблона из: {excel_path}...") + df = pd.read_excel(excel_path) + df_str = df.astype(str) # Сразу приводим к строкам для быстрого поиска + + # 2. Группируем входящие коды (КМ) по GTIN / Ключу + # Структура: { '1234567890123': [(raw_bytes_1, b64_1), (raw_bytes_2, b64_2)] } + grouped_codes = defaultdict(list) + for b64 in base64_codes: + try: + raw_bytes, search_key = extract_key_from_base64(b64) + grouped_codes[search_key].append(raw_bytes) + except Exception as e: + print(f"Ошибка парсинга кода {b64[:10]}... : {e}") + + font_name = init_pdf_font() + + # 3. Оркестрация: 1 GTIN = 1 Многостраничный PDF файл + for search_key, raw_bytes_list in grouped_codes.items(): + print(f"\nНачинаем сборку PDF для GTIN: {search_key} (кодов: {len(raw_bytes_list)})") + + # 3.1. Ищем структуру для этого GTIN в таблице + mask = df_str.apply(lambda row: row.str.contains(search_key).any(), axis=1) + matched_rows = df[mask] + + if matched_rows.empty: + print(f" [ПРОПУСК] GTIN {search_key} не найден в Excel. Этикетки не сгенерированы.") + continue + + target_row = matched_rows.iloc[0] + + # Загружаем лимитированную структуру (Dataclass) + label_data = LabelData.from_excel_row(target_row) + + # 3.2. Создаем холст PDF для группы + pdf_path = os.path.join(output_dir, f"Labels_{search_key}.pdf") + # Размер страницы 58х40 мм + c = canvas.Canvas(pdf_path, pagesize=(58*mm, 40*mm)) + + # 3.3. Рендерим каждую этикетку как отдельную страницу в документе + for i, raw_bytes in enumerate(raw_bytes_list): + label_data.page_num = i + 1 + # Рендер DM полностью в оперативной памяти (без I/O диска) + dm_image = create_datamatrix_in_memory(raw_bytes) + # Декодируем сырые байты в читаемую строку (игнорируя непечатаемый GS/FNC1) + # и берем первые 31 символ (обычно это: 01 + 14 GTIN + 21 + 13 Серийник) + readable_km = raw_bytes.decode('utf-8', errors='ignore')[:31] + + # Вставляем спасительный пробел после 16-го символа + readable_km = readable_km[:16] + ' ' + readable_km[16:] + + # Рисуем страницу, передавая ПОЛНЫЙ КОД МАРКИРОВКИ вместо короткого search_key + draw_label_page(c, font_name, dm_image, label_data, readable_km) + + # Сохраняем и закрываем сгенерированный PDF документ + c.save() + print(f" -> Сохранен файл: {pdf_path}") + + +if __name__ == "__main__": + # Тестовый пример оркестрации: + base_dir = os.path.dirname(os.path.abspath(__file__)) + image_path = "data/output.png" + data = read_datamatrix_zxing(image_path) + # Замените своими боевыми Base64 строками + mock_base64_list = [ + read_datamatrix_zxing(image_path), + read_datamatrix_zxing(image_path) + ] + + process_batch( + base64_codes=mock_base64_list, + excel_path=os.path.join(base_dir, "resources", "ШАблон для загрузки этикеток.xlsx"), + output_dir=os.path.join(base_dir, "data", "output_pdfs") + ) \ No newline at end of file diff --git a/read_image.py b/read_image.py new file mode 100644 index 0000000..9990967 --- /dev/null +++ b/read_image.py @@ -0,0 +1,36 @@ +import zxingcpp as zxing_cpp +from PIL import Image +import base64 + +def read_datamatrix_zxing(file_path: str) -> str: + # Открываем изображение через Pillow + img = Image.open(file_path) + + # Читаем штрих-код. + # zxing_cpp.read_barcodes умеет работать напрямую с объектами PIL + results = zxing_cpp.read_barcodes(img) + + if not results: + print("Коды не найдены на изображении.") + return '' + + # Берем первый найденный код + result = results[0] + + # Извлекаем именно байты (важно для непечатаемых символов GS/FNC1) + raw_bytes = result.bytes + + # Кодируем байты в Base64 + # b64encode возвращает bytes, поэтому делаем .decode('ascii') для получения строки + b64_string = base64.b64encode(raw_bytes).decode('ascii') + + return b64_string + +if __name__ == "__main__": + image_path = "data/output.png" + data = read_datamatrix_zxing(image_path) + + if data: + print(f"Успешно прочитано: {data}") + else: + print("Код не распознан") \ No newline at end of file diff --git a/render_eps.py b/render_eps.py new file mode 100644 index 0000000..4305310 --- /dev/null +++ b/render_eps.py @@ -0,0 +1,25 @@ +import os +import PIL.EpsImagePlugin + +# 1. ПОРТАТИВНОСТЬ: Вычисляем путь относительно этого скрипта +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +gs_path = os.path.join(BASE_DIR, 'Ghostscript', 'bin') + +# Добавляем в PATH +os.environ['PATH'] = gs_path + os.pathsep + os.environ.get('PATH', '') + +# 2. ХАК: Принудительно заставляем Pillow использовать 32-битную версию +# Мы устанавливаем имя бинарника ДО того, как Pillow начнет его искать +PIL.EpsImagePlugin.gs_binary = "gswin32c" + +from PIL import Image + +file_path = './data/d46349f7-148a-4301-b6b5-f9a3c70fdf19_04639970975115_2000.eps' + +try: + img = Image.open(file_path) + img.load(scale=10) + img.save('./data/output.png', 'PNG') + print("Успех! Файл сохранен в ./data/output.png") +except Exception as e: + print(f"Опять ошибка: {e}") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e994d49 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +pillow +zxing-cpp +reportlab +pandas +treepoem +openpyxl \ No newline at end of file diff --git a/resources/eac-conformity-mark-seeklogo.png b/resources/eac-conformity-mark-seeklogo.png new file mode 100644 index 0000000..5fc3f8e Binary files /dev/null and b/resources/eac-conformity-mark-seeklogo.png differ diff --git a/resources/logo-CHZ-grey.png b/resources/logo-CHZ-grey.png new file mode 100644 index 0000000..ece33c7 Binary files /dev/null and b/resources/logo-CHZ-grey.png differ diff --git a/resources/ШАблон для загрузки этикеток.xlsx b/resources/ШАблон для загрузки этикеток.xlsx new file mode 100644 index 0000000..80c51d1 Binary files /dev/null and b/resources/ШАблон для загрузки этикеток.xlsx differ