Хабрахабр

Печать гобелена «Игры престолов» на фискальном принтере с использованием Python

Однажды в одном из проектов в мои руки попал фискальный принтер. Мы каждый день сталкиваемся с этими устройствами, когда совершаем платежи в магазинах, но мало кто догадывается что на самом деле они из себя представляют. Не буду вдаваться в подробности их работы, просто скажу, что это такие штучки, которые печатают чеки с данными о покупке на специальной термобумаге (да-да, почти во всех фискальных принтерах нет чернил!).

Задача давно выполнена, а фискальный принтер был надолго заброшен в дальний угол… Пока в мою голову не пришла идея немного покреативить 😀 Я должен был разобраться как получить состояние функционирования фискального принтера и его внутренние параметры настройки.

Когда я вдоволь наигрался с печатью котиков, эмблем и фотографий коллег, я решил замахнуться на печать длинного гобелена по мотивам сериала, в котором постоянно кого-то убивали со словами «зима близко». Такие принтеры позволяют печатать монохромные картинки.

На выходе получился вот такой ролик:

Подробные действия для печати гобелена на языке python под катом ниже.
В этой статье я коротко опишу порядок действий с подробными комментариями, полагая, что на выходе получится обучающая статья с несколькими полезными практическими навыками:

  • скачиваем видео ролик гобелена с youtube
  • создаём длинное монохромное изображение гобелена для печати на фискальном принтере
  • подключаемся к фискальному принтеру
  • печатаем гобелен на фискальном принтере
  • монтируем получившийся видео ролик для публикации в соц сетях

Скачиваем видео ролик гобелена с youtube

Делается очень просто с помощью библиотеки pytube, нужно лишь определиться с индексом видео потока, который собираемся скачать.

Функция для скачивания видео ролика с youtube:

import time, pytube # оформляем в виде функции для простого многократного использования
def load_bmp_from_video(video_url): t1 = time.clock() # подготавливаем объект видео ролика video = pytube.YouTube(video_url) # печатаем доступные потоки для скачивания внутри видеоролика streams = video.streams.all() for stream in streams: print(stream) # сохраняем нужный видео поток в указанный файл,используем индекс 18: 360p mp4 video.streams.get_by_itag(18).download("./", filename = filename ) t2 = time.clock() # замеряем время скачивания в секундах print('download done', t2-t1) # ролик гобелена будет скачен в файл got.mp4 с разрешением 360x640
load_bmp_from_video(video_url = 'https://www.youtube.com/watch?v=aZV4PclhHeA&', filename = 'got')

При выполнении строки for stream in streams: print(stream) увидим в консоли перечень всех видео потоков, содержащихся в ролике:

<Stream: itag="22" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.64001F" acodec="mp4a.40.2">
<Stream: itag="43" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp8.0" acodec="vorbis">
<Stream: itag="18" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.42001E" acodec="mp4a.40.2">
<Stream: itag="137" mime_type="video/mp4" res="1080p" fps="30fps" vcodec="avc1.640028">
<Stream: itag="248" mime_type="video/webm" res="1080p" fps="30fps" vcodec="vp9">
<Stream: itag="136" mime_type="video/mp4" res="720p" fps="30fps" vcodec="avc1.4d401f">
<Stream: itag="247" mime_type="video/webm" res="720p" fps="30fps" vcodec="vp9">
<Stream: itag="135" mime_type="video/mp4" res="480p" fps="30fps" vcodec="avc1.4d401e">
<Stream: itag="244" mime_type="video/webm" res="480p" fps="30fps" vcodec="vp9">
<Stream: itag="134" mime_type="video/mp4" res="360p" fps="30fps" vcodec="avc1.4d401e">
<Stream: itag="243" mime_type="video/webm" res="360p" fps="30fps" vcodec="vp9">
<Stream: itag="133" mime_type="video/mp4" res="240p" fps="30fps" vcodec="avc1.4d4015">
<Stream: itag="242" mime_type="video/webm" res="240p" fps="30fps" vcodec="vp9">
<Stream: itag="160" mime_type="video/mp4" res="144p" fps="30fps" vcodec="avc1.4d400c">
<Stream: itag="278" mime_type="video/webm" res="144p" fps="30fps" vcodec="vp9">
<Stream: itag="140" mime_type="audio/mp4" abr="128kbps" acodec="mp4a.40.2">
<Stream: itag="249" mime_type="audio/webm" abr="50kbps" acodec="opus">
<Stream: itag="250" mime_type="audio/webm" abr="70kbps" acodec="opus">
<Stream: itag="251" mime_type="audio/webm" abr="160kbps" acodec="opus">

Я выбрал поток с идентификатором 18 т.к. он небольшого разрешения — нам всё равно печатать его на чековой ленте, да и качать быстрее))

Создаём длинное монохромного изображение гобелена для печати на фискальном принтере

Для обработки видео нам понадобится известная OpenСV библиотека и Pillow (современный форк PIL) (хотя здесь вместо OpenCV можно было бы использовать утилиту avconv из состава инструмента libav, более подробно о ней в последнем заголовке данной статьи). Большое спасибо автору за написание python либы python-opencv, которая представляет собой python wheel, ставиться через PIP и не требует установки самой OpenCV (ура!).

Кроме того, изображение гобелена в видео ролике постоянно движется, поэтому нам нужно аккуратно нарезать кадры так, чтобы получилась одна длинная картинка. Фискальный принтер может печатать только особые изображения — монохромные bmp файлы фиксированной ширины в 528 пикселей (зато неограниченной длины, хо-хо-хо!).

Всё это делает следующая функция:

import os, cv2, numpy as np
from PIL import Image # нарезаем скриншоты из видео в изображение для печати на фискальном принтере
def save_frames_from_vide(filename, delta = 1): # имя файла без расширения понадобится для сохранения финального изображения real_filename = filename.rsplit('.', 1)[0] # для удобства удаляем все старые скрины перед началом for file in os.listdir('./'): if file.startswith('frame'): os.remove('./' + file) # нарезаем нужные кадры в список для последующей склейки frames_list = [] vidcap = cv2.VideoCapture(filename) try: success, frame = vidcap.read() count = 1 while success: # and count < 500: # это для отладки # первые скрины на позициях 1 и 100, дальше сцена начинает двигаться if count in [1, 100, 30945, 31000] or count % 370 == 0: # калибруем третий и предпоследний скрины (начало и окончание) mono_frame = frame if count == 370: mono_frame = mono_frame[0:mono_frame.shape[0], 172:mono_frame.shape[1]] if count == 30710: mono_frame = mono_frame[0:mono_frame.shape[0], 0:mono_frame.shape[1] - 200] mono_frame = mono_frame[20:-20, :] frames_list.append(mono_frame) print('read a new frame: ', success, count) success, frame = vidcap.read() count += 1 finally: vidcap.release() # склеиваем все нарезанные кадры gobelin = np.concatenate((frames_list), axis = 1) # сохраняем результат - цветной длинный гобелен cv2.imwrite('%s.png' % real_filename, gobelin) # преобразуем картинку для печати на чековой ленте image = Image.open('%s.png' % real_filename) # делаем 1 байт на цвет пикселя чтобы получить монохромный bmp fn = lambda x : 255 if x > 135 else 0 image = image.convert('L').point(fn, mode = '1') # растягиваем картинку до ширины ленты в 528 пикселей coef = 528. / image.height new_w = int(image.width * coef) new_h = int(image.height * coef) image = image.resize((new_w, new_h)) # вращаем гобелен на 270 градусов для печати вертикально и с нужной стороны image = image.transpose(Image.ROTATE_270) image.save('%s_for_print.bmp' % real_filename) # сохраняем картинки с видеоролика в файлы 'got.png' и 'got_for_print.bmp'
save_frames_from_vide('got.mp4')

На выходе получаем длинную картинку с изображением всего гобелена, ниже показан лишь фрагмент, оригинал картинки имеет ширину в 55000 пикселей и не проходит по правилам публикации:

А вот такое изображение получается после монохромных преобразований, только без поворота:

Печатаем гобелен на фискальном принтере

В моём распоряжении конкретный фискальный принтер модели Атол fprint-22, но общие правила распространяются и на другие модели фискальных принтеров. Причём мой фискальник довольно древний и ещё не поддерживает новомодные требования ФЗ-54 (напомню, что после вступления в силу этого закона все фискальники обязали отправлять данные через ОФД в налоговую, что повлекло за собой боль и страдания — перепрошивку каждого устройства).

Они относятся к POS устройствам — это всевозможная периферия для нужд торговли, которая подключаются к ПК и интегрируются в единую систему учёта и оплаты. Небольшое отступление о фискальных принтерах. Для всех этих устройств был придуман унифицированный протокол взаимодействия UnifiedPOS. Из подобных известных устройств вы точно видели сканеры штрихкодов и терминалы оплаты банковскими картами.

Ситуацию осложняет тот факт, что большинство этих устройств заточено под функционирование исключительно под ОС Windows через COM объекты — dll файлы с очень плохим документальным описанием функциональных возможностей. Короче, это отдельная тема и очень узкий круг специалистов, занимающихся POS устройствами. Хотя я слышал, про кассовые системы, работающие под FreeBSD, но за время работы с POS устройствами ничего такого не встретил, благо, что от меня не требовалось детальное погружение в мир Retail POS бизнес-процессов…

Поэтому порядок работы с большинством таких девайсов следующий:

  • устанавливается драйвер от производителя
  • настраивается подключение к девайсу через утилиту производителя
  • ищется нужный раздел реестра с нужным устройством
  • ищутся нужные параметры подключения к нему
    (большинство работают по последовательному программному интерфейсу RS-232)
  • осуществляется подключение к устройству через COM объект драйвера производителя
  • осуществляется работа с устройством через API COM объекта
  • осуществляется освобождение COM объекта и физического порта устройства
    (важный момент)

Поскольку в моём распоряжении древний фискальник, то драйвер к нему используется именно 8-ой версии. Сейчас производитель добавил драйвер 10-ой версии, который намного упрощает работу с фискальным принтером через отдельный python wrapper-модуль, что не может не радовать.

Нам потребуется установка pywin32. Следующий код демонстрирует функцию, которая подключается к фискальному принтеру по выше описанному алгоритму, производит гудок и печатает полученное ранее монохромное изображение гобелена.

Код получился довольно длинным и скучным, так что я вынес его под спойлер:

import win32com.client
from _winreg import HKEY_CURRENT_USER, OpenKey, EnumValue # подключаемся к фискальнику через COM объект драйвера, # виртуальный COM порт и напечатать картинку
def fiscal_print(filename): driver = None try: # получаем содержимое специального раздела реестра # для фискального регистратора фирмы АТОЛ для драйвера версии 8.16.х try: hKey = OpenKey(HKEY_CURRENT_USER, r"Software\Atol\Drivers\8.0\KKM\Devices") except Exception as err: raise FiscallError('не удалось прочитать раздел реестра с ' + 'параметрами подключения ' + 'к устройству Атол FPrint22-ПТК') # получаем значение единственной переменной в этом разделе, # в которой хранятся параметры подключения к com порту try: device_name,device_connect_params,device_connect_dt=EnumValue(hKey,0) except Exception as err: raise FiscallError('не удалось прочитать переменную реестра ' + 'с параметрами подключения к драйверу ' + 'устройства Атол FPrint22-ПТК') # разбираем все параметры подключения в словарь try: connect_dir = dict([tup.split(u'=') for tup in device_connect_params]) except Exception as err: raise FiscallError('не удалось распарсить параметры подключения ' + 'к драйверу устройства Атол FPrint22-ПТК') # подключаемся к нужному COM объекту try: driver = win32com.client.Dispatch("AddIn.FPrnM8") except Exception as err: raise FiscallError('нужный COM объект AddIn.FPrnM8 для устройства ' + 'Атол FPrint22-ПТК не найден в ОС, ' + 'проверьте наличие и версию драйвера') # добавляем логическое устрйоство и параметры подключения к нему add_code = driver.AddDevice() if driver.ResultCode != 0: raise FiscallError('ошибка взаимодействия с ККМ Атол FPrint22-ПТК' + ' [код %s] - %s'% (driver.ResultCode, driver.ResultDescription)) # регистрируем параметры подключения к драйверу через COM портs driver.Model = connect_dir['Model'] driver.PortNumber = connect_dir['PortNumber'] driver.UseAccessPassword = connect_dir['UseAccessPassword'] driver.DefaultPassword = connect_dir['UseAccessPassword'] driver.PortNumber = connect_dir['PortNumber'] driver.BaudRate = connect_dir['BaudRate'] # после этой операции ККМ занимает порт и открывает # по сути физический канал связи по COM порту driver.DeviceEnabled = 1 # поулчаем значения основных параметров ККМ из вызова метода GetStatus, # полный перечень всех атрибутов расписан на стр. 61 к руководству по v8.0 res = driver.GetStatus() if driver.ResultCode != 0: raise FiscallError('ошибка взаимодействия с ККМ Атол FPrint22-ПТК ' + '[код %s] - %s' % (driver.ResultCode, driver.ResultDescription)) ### выполняем дейсвтия # просто гудок driver.Beep() # максимальная ширина, доступная для печати в пикселях (528) print('driver.PixelLineLength:', driver.PixelLineLength) # !!! настраиваем параметры печати через атрибуты класса # (так принято работать с COM объектами) driver.Alignment = 0 driver.LeftMargin = 0 driver.PrintPurpose = 1 driver.AutoSize = False driver.Scale = 100 # указываем имя файла для печати driver.FileName = filename # собственно печать монохромного bmp файла driver.PrintBitmapFromFil for i in range(10): driver.PrintString() driver.FullCut() # всё! except FiscallError as err: raise err except Exception as err: raise FiscallError('внутренняя ошибка функции взаимодейсвтия ' + 'с ККМ Атол FPrint22-ПТК - %s' % str(err)) finally: if driver: driver.DeviceEnabled = 0 fiscal_print('got_for_print'.bmp)

На выходе получился такой вот манускрипт, перформанс на «Песнь льда и пламени»:

Принтер под конец совсем взвыл, печатал медленно и тускло, а потом и вовсе допечатал только финальную сцену — такой нагрузки ещё ни один фискальный принтер не видел 😀

В качестве аудиоряда я нашёл в сети 8bit любительскую композицию — заглавную тему сериала. Осталось подготовить видео и запостить в соц сети. Идея была в том, чтобы наложить одно с другим вообще без использования видео редактора, об этом пишу ниже в заключительной части статьи.

Монтируем получившийся видео ролик для публикации в соц сетях

Для этих целей существует очень полезный и мощный консольный инструмент, заменяющий целый видео редактор — libav. В его составе есть утилиты avconf и ffmpeg для работы с видео и аудио файлами. Честно говоря, для меня этот инструмент стал настоящим открытием, всем рекомендую!

Основная идея монтажа:

  • вырезать из отснятого на смартфон ролика часть в начале и часть в конце
    (подогнать под 3 проигрыша mp3 файла с 8bit музыкой)
  • записать 3 проигрыша аудио файла в один файл
  • наложить видео файл и аудио файл в новый видео файл
  • сконвертировать видео файл из формата mov в формат mp4
    (мой смартфон снимает ролики с расширением mov

Для этих целей я написал скрип для запуска в командой строке, который может выполняться как bash в linux, так и bat в win (различия указаны в комментариях скрипта):

# обрежем ролик в начале
avconv -ss 00:00:10 -i got_print.mov -t 00:06:00 -c:v copy got_print_tmp.mov # подготовим файл для зацикливания музыки (под win)
(echo file 'got_8bit.mp3' & echo file 'got_8bit.mp3' & echo file 'got_8bit.mp3') > list.txt # подготовим файл для зацикливания музыки (под linux)
# cat list.txt
# file 'got_8bit.mp3'
# file 'got_8bit.mp3'
# file 'got_8bit.mp3' # зациклим музыкальный файл в 3 раза
ffmpeg -f concat -i list.txt -acodec copy got_8bit_3.mp3 # наложим видео и звук в один финальный ролик
avconv -i got_print_tmp.mov -i got_8bit_3.mp3 -c copy got_print_final.mov # конвертируем выходной видео файл в формат mp4
avconv -i got_print_final.mov -c:v libx264 got_print_final.mp4 # удалим лишние файлы (windows)
del got_8bit_3.mp3
del got_print_tmp.mov # удалим лишние файлы (linux)
# rm got_8bit_3.mp3
# rm got_print_tmp.mov

Вот и всё, ролик создан:

P.S.: моя первая статья на Хабре, планировал написать короткую статью для начала, сокращал как мог) надеюсь, что чтение было приятным, а результат моей работы — интересным)

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть