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:
166
.gitignore
vendored
Normal file
166
.gitignore
vendored
Normal 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
BIN
Ghostscript.zip
Normal file
Binary file not shown.
56
README.md
Normal file
56
README.md
Normal 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
418
build_pdf.py
Normal 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
36
read_image.py
Normal 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
25
render_eps.py
Normal 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
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
pillow
|
||||
zxing-cpp
|
||||
reportlab
|
||||
pandas
|
||||
treepoem
|
||||
openpyxl
|
||||
BIN
resources/eac-conformity-mark-seeklogo.png
Normal file
BIN
resources/eac-conformity-mark-seeklogo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
BIN
resources/logo-CHZ-grey.png
Normal file
BIN
resources/logo-CHZ-grey.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
resources/ШАблон для загрузки этикеток.xlsx
Normal file
BIN
resources/ШАблон для загрузки этикеток.xlsx
Normal file
Binary file not shown.
Reference in New Issue
Block a user