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.
This commit is contained in:
2026-02-21 11:46:24 +03:00
commit 93b28b1ea2
10 changed files with 707 additions and 0 deletions

166
.gitignore vendored Normal file
View File

@@ -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

BIN
Ghostscript.zip Normal file

Binary file not shown.

56
README.md Normal file
View File

@@ -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 диска.

418
build_pdf.py Normal file
View File

@@ -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', '<br/>')
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")
)

36
read_image.py Normal file
View File

@@ -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("Код не распознан")

25
render_eps.py Normal file
View File

@@ -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}")

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
pillow
zxing-cpp
reportlab
pandas
treepoem
openpyxl

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
resources/logo-CHZ-grey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB