Как генератор трёх слов на баннере вырос в веб-сервис: Ghostscript, CMYK и производственные грабли

Я графический дизайнер на фрилансе. За полноценный макет — бренд-волл, сложную афишу, баннер с графикой и фотомонтажом — средний ценник начинается от 1 500–3 000 рублей. И это оправдано: там есть композиция, подбор исходников, сетка, ретушь и подготовка под конкретный носитель.
Но на практике регулярно приходят заказчики с совсем другой задачей:
«Сделай баннер на забор: жёлтый фон, крупно АРЕНДА и номер телефона» или «Нам на фасад растяжку: МЫ ОТКРЫЛИСЬ!»
Просить за такое 2 000 рублей неловко — сложно объяснить и клиенту, и себе, за что тут платить, когда дизайнерской работы ровно на ноль секунд. Но когда заказчик пытается сделать макет сам в подручных средствах — от Word до бесплатных онлайн-редакторов, — в типографию обычно прилетает растровый JPEG в RGB без соблюдения пропорций и отступов, где текст стоит вплотную к краю и попадает прямо под пробивку металлических колец (люверсов).
В итоге для такой задачи всё равно нужен не просто экспорт картинки, а файл с точными габаритами в миллиметрах ($1:1$), предсказуемыми значениями CMYK, текстом в кривых и выверенными полями под послепечатную обработку. Поэтому раньше мне приходилось каждый раз включать ноутбук, запускать CorelDRAW или Illustrator, вручную выставлять размеры, набирать три слова, переводить шрифты в кривые и экспортировать PDF. Времени на механическую возню уходило почти столько же, сколько на нормальный заказ.
В какой-то момент эта рутина надоела. Сначала я написал внутренний скрипт для себя: вбил текст в форму, нажал кнопку — через секунду забрал векторный PDF для типографии. Постепенно скрипт вырос в производственный сервис - конструктор макетов за стоимость чашки кофе с проверкой выходных файлов, оплатой, пакетной генерацией из CSV для агентств, бесшовным режимом для типографий и более чем 1 100 автотестами в CI. Код проекта опубликован на GitHub.
В этой статье расскажу, почему главная сложность оказалась не в том, чтобы нарисовать три слова на цветном фоне, а в том, чтобы гарантировать предсказуемый печатный файл, не потерять заказ при обрыве связи и встроить сервис в процесс реальных типографий.
Архитектура и стек
Сервис задумывался как лёгкий автономный монолит с минимумом внешних зависимостей. Бэкенд написан на Python 3.11 с FastAPI 0.110 и Pydantic 2. В продакшене работает один воркер Uvicorn и пул из двух процессов (ProcessPoolExecutor) под задачи Ghostscript: сборка PDF ощутимо нагружает процессор, поэтому прямой синхронный вызов из event loop блокировал бы обработку остальных запросов. За хранение отвечает SQLite в режиме WAL — для нашей нагрузки тянуть Postgres или внешние очереди было бы избыточно, тогда как SQLite работает без сетевых задержек и бэкапится простым копированием файла.
Генерация графики разделена на два контура. Пока пользователь меняет текст, размер и цвета в браузере, нужен быстрый отклик, поэтому превью собирается отдельной лёгкой веткой на Pillow в JPEG с диагональным водяным знаком (в печатном файле его нет). Сейчас картинка уходит на фронт как base64 внутри JSON — это на треть тяжелее бинарного ответа, так что перевод ручки на прямой image/jpeg лежит в списке ближайших доработок. Сам печатный PDF строится связкой ReportLab и Ghostscript 10: первый формирует первичную векторную сцену, а второй переводит шрифты в кривые и фиксирует цветовую модель CMYK.
Фронтенд написан на обычных HTML, CSS и чистом JS без фреймворков и сборщиков. Всё это упаковано в Docker Compose за Nginx 1.25 (HTTP/2, статика, rate-лимиты) и деплоится через GitHub Actions, где на каждый пуш прогоняются ruff и pytest. На уровне Nginx публичные ручки закрыты лимитами по IP: превью — 30 запросов в минуту (всплеск до 10), создание заказа — 5, скачивание — 10, проверка промокода — 20. Если пользователь быстро правит текст и упирается в лимит превью, фронтенд сам повторяет запрос через 2,5, 5 и 10 секунд, отменяя ожидание при новом вводе.
Грабли №1: разрешение устройства pdfwrite в Ghostscript, предел 32 767 и геометрия кривых
Уличные баннеры измеряются в метрах: 2 х 2 м, 3 х 3 м. Внутри PDF базовая единица измерения — типографский пункт (1 = 1/72 inch), поэтому трёхметровое полотно даёт около 8 504 pt по стороне.
В конвейере первичный PDF из ReportLab передаётся устройству pdfwrite в Ghostscript (версии 10.02–10.05) с флагом -dNoOutputFonts, который превращает глифы текста в векторные контуры Безье. И здесь вылезла интересная особенность работы pdfwrite: внутренние координаты узлов в выходном потоке страницы масштабируются через параметр разрешения устройства -r ($\text{pt} \cdot \text{dpi} / 72$). По умолчанию у Ghostscript это -r720, из-за чего в начале потока ставится матрица масштабирования 0.1 0 0 0.1 cm, а сами координаты умножаются на 10. На двухметровом баннере числа в потоке страницы улетали за 56 692:
% Фрагмент потока Ghostscript при -r720, баннер 2000x750 мм:
0.1 0 0 0.1 cm
... 56692.9 ...
Чтобы проверить, как к такому файлу отнесутся старые парсеры и строгие чекеры, я прогнал его через валидатор veraPDF (использовал профиль PDF/A-1 как самый жёсткий публичный детектор ограничений эпохи PDF 1.4) и сразу получил ошибку: Absolute real value must be less than or equal to 32767.0. Хотя современный стандарт ISO 32000 допускает куда больший диапазон вещественных чисел, порог 32 767 (знаковое 16-битное целое) остаётся ограничением совместимости в ряде старых реализаций PDF и препресс-валидаторов.
Просто снизить параметр -r до 72 или 144 для всех заказов оказалось нельзя: при конвертации шрифтов в кривые на грубой сетке опорные точки контуров начинают смещаться. Я сравнил координаты вершин сгенерированных кривых с эталоном (-r720) на наших шрифтах и замерил максимальное расхождение в миллиметрах:
Размер баннера | -r144 | -r249 | -r374 |
|---|---|---|---|
3000×3000 мм | 1,95 мм | 0,50 мм | — |
2000×750 мм | 0,25 мм | — | 0,05 мм |
Чтобы сохранить точность контуров на малых размерах и не превысить порог 32 767 на трёхметровых полотнах, параметр -r для Ghostscript рассчитывается динамически под габарит конкретного заказа — выбираем максимальное разрешение (до 720), при котором числа в потоке остаются в пределах 90% от лимита:
COORD_LIMIT = 32767
_GS_MAX_DPI = 720
_GS_NUMBER_BUDGET = 0.9 # держим числа в потоке не выше 90% от 32767
def _output_dpi(width_mm: float, height_mm: float) -> int:
side_pt = max(width_mm, height_mm) * 72 / 25.4
return int(max(72, min(_GS_MAX_DPI, COORD_LIMIT * _GS_NUMBER_BUDGET * 72 / side_pt)))
В результате макеты до 1 040 мм обрабатываются при -r720, двухметровые — при -r374, а трёхметровые — при -r249. Координаты в потоке не превышают 29 500, а смещение контуров букв на трёхметровом баннере укладывается в 0,5 мм.
Грабли №2: почему было решено отказаться от PDF/X и встроенного профиля ISO Coated v2
В общих руководствах по полиграфии часто требуют: «Встраивайте OutputIntent с профилем ISO Coated v2 (FOGRA39) и отдавайте PDF/X». В первых версиях я так и сделал — включил режим -dPDFX с подшивкой профиля ISOcoated_v2_300_eci.icc. Векторный файл из 4 КБ сразу вырос до 1,4–1,8 МБ за счёт встроенного профиля. Однако при детальном разборе специфики широкоформатной печати от этой схемы пришлось отказаться.
Дело в том, что словарь OutputIntent в PDF фиксирует целевые условия вывода, а универсальный профиль ISO Coated v2 описывает офсетную печать на мелованной бумаге. Баннерная же ткань (ламинированный или литой ПВХ, сетка) печатается сольвентными, экосольвентными или УФ-чернилами, где оператор RIP-станции всегда использует собственный профиль, построенный под конкретную связку «станок + чернила + материал». К тому же цветовой профиль реально необходим при конвертации полноцветных RGB-фотографий в CMYK, а в нашем конструкторе растра нет вообще — только векторный текст и плоская плашка фона, цвета которых ReportLab изначально задаёт напрямую в DeviceCMYK. Ради интереса я сгенерировал макеты для всех 30 цветовых пар палитры с флагами ICC-профиля и без них: потоки содержимого страниц совпали побайтово.
По стандарту нельзя называть файл PDF/X, если в нём отсутствует OutputIntent. Поэтому сознательно не заявляется PDF/X: генератор отдаёт чистый DeviceCMYK с фиксированными рецептами цветов (например, красный 0 100 100 0, жёлтый 0 0 100 0) и передаёт управление воспроизведением красок RIP-системе типографии.
Попутно в генератор было заложено ещё несколько технологических ограничений. Чёрный текст всегда выводится чистым K100 (0 0 0 100), поскольку на буквах составной чёрный из четырёх красок при малейшем несведении головок станка может давать цветной ореол. Для собственной палитры также установлен консервативный предел суммы красок (TAC) в 200% при пороге внутренней проверки 240% — допустимый TAC в широком формате сильно зависит от сушки конкретного станка, поэтому сдержанные двухкомпонентные рецепты оказались самым надёжным вариантом. Наконец, сам Ghostscript запускается с флагами -dPDFSETTINGS=/prepress -dCompatibilityLevel=1.4 -dNoOutputFonts -sColorConversionStrategy=CMYK -dProcessColorModel=/DeviceCMYK. Версия PDF 1.4 зафиксирована намеренно: начиная с PDF 1.5 свежие версии pdfwrite по умолчанию упаковывают словари в сжатые объектные потоки (WriteObjStms), а нам важно сохранять плоскую и прозрачную структуру файла для быстрого внутреннего контроля.
Инспектор pdf_check: проверяем и файл, и обещания на сайте
Поскольку Ghostscript работает как внешний процесс, полагаться только на нулевой код возврата нельзя. После каждой сборки PDF проходит через внутренний инспектор pdf_check на стандартной библиотеке Python. Он проверяет выполнение нашего собственного контракта перед покупателем:
в файле ровно одна страница, а её
MediaBoxсовпадает с заказанным размером в миллиметрах с допуском до 0,5 pt (около 0,2 мм) — то есть масштаб $1:1$;внутри нет словарей
/Font(весь текст переведён в кривые) и нет растровых XObject-изображений;в потоке страницы и внутри
Form XObjectнет цветовых пространств/DeviceRGBи операторовrg/RG;отсутствуют
OutputIntentи сжатые потоки/ObjStm;ни одна координата в потоке не превышает 32 767, а сумма красок в операторах
k/Kне выходит за 240%.
В рантайме pdf_check работает в режиме неблокирующего аудита: если обнаружится отклонение, он запишет предупреждение в лог администратору, но файл оплатившему клиенту всё равно отдаст. А в CI эти же проверки блокируют сборку при любой регрессии.
Зеркальный тест (test_static_site.py) проверяет HTML-код всех страниц сайта и падает, если в текстах вдруг появятся слова ICC, ISO Coated, PDF/X, FOGRA или ГОСТ. Так тесты следят, чтобы описание на сайте не расходилось с реальным содержимым файла.
Грабли №3: почему превью в браузере и PDF разъезжались по краям
Покупатель утверждает макет по JPEG-превью в браузере, поэтому векторный PDF должен совпадать с ним геометрически. Но на стыке Pillow и ReportLab вскрылась разница в расчёте границ строки. Pillow центрировал строку по пиксельному боксу реально закрашенных глифов, тогда как ReportLab ставил её по типографской ширине символов (advance width) и базовой линии (baseline), не учитывая боковые вылеты и нижние выносные элементы.
Пока макеты набирали простыми гротесками в верхнем регистре, разница оставалась минимальной. Но когда добавился рукописный шрифт Caveat и строчные буквы с хвостами («р», «д», «у») или наклонный восклицательный знак, в итоговом PDF безопасное поле в 30 мм справа схлопнулось до 0 мм, а снизу — до 6 мм. Крайняя буква легла прямо на границу реза, где в типографии пробивают люверсы.
Пришлось свести геометрию обоих движков к единой функции inkmetrics, вычисляющей видимые границы строки в долях кегля. В процессе всплыли две тонкости: метод textbbox в Pillow возвращал левый отступ как 0 вместо реального левого выступа первой буквы, поэтому крайние символы строки теперь измеряются растеризацией глифа с запасом по полям. Кроме того, ReportLab выводит текст без кернинга, из-за чего горизонтальная ширина строки берётся из метрик ReportLab, а вертикальные границы (верхнюю точку и нижние хвосты) — из Pillow. Регрессионный тест теперь прогоняет 15 комбинаций (5 х 3) через полный цикл Ghostscript и проверяет симметричность полей в 30 мм слева и справа.
Что происходит при настоящем заказе: оплата, обрыв связи и право на опечатку
При низкой цене макета ручной разбор одного возврата или претензии стоит дороже самого заказа. Поэтому клиентский путь спроектирован так, чтобы гасить типичные проблемы автоматически.
Всё начинается с экрана сверки перед оплатой: кнопка «Получить PDF» сначала открывает окно с крупным текстом баннера, размером и обязательным чекбоксом согласия с офертой. В базу при этом сохраняются снимок параметров макета, версия оферты, время акцепта, IP и User-Agent. Сами готовые PDF сервер на диск не складывает — в БД хранится только JSON-конфигурация заказа, по которой макет собирается на лету при скачивании. Через 14 дней фоновый процесс стирает текст макета, IP и User-Agent из базы, после чего запрос по старой ссылке возвращает 410 Gone.
Отдельно пришлось защитить токен скачивания от сбоев сети. Ссылка действует 15 минут и даёт до трёх попыток. В первой версии токен гасился ещё до запуска рендера: если Ghostscript падал с ошибкой или у клиента в метро обрывалась связь, попытка сгорала. Сейчас токен сначала проверяется чтением, затем рендерится PDF, и только когда файл готов к отправке, выполняется один атомарный запрос, который увеличивает счётчик выдач (downloads = downloads + 1), помечает токен исчерпанным на третьей попытке (used = CASE WHEN downloads + 1 >= 3 THEN 1 ELSE 0 END) и проверяет условие WHERE token = ? AND used = 0 AND downloads < 3 через rowcount. Сбой рендера попытку не тратит, а запас в три скачивания спасает при нестабильном мобильном интернете.
Также у пользователя есть право на бесплатную правку текста в течение 24 часов. Частая ситуация: человек оплатил заказ, скачал файл и заметил опечатку в номере телефона. Через эндпоинт /api/order/{id}/amend в течение суток после оплаты можно один раз бесплатно изменить текст строк. Размер, шрифт, цвета и число строк при этом заблокированы, чтобы одну покупку не превратили в генератор новых баннеров. Правка захватывается атомарным UPDATE ... WHERE amended_at IS NULL, исходный вариант сохраняется в original_config_json на случай спора, а покупатель получает новый токен на исправленный PDF.
Пакетная генерация из CSV, промокоды и партнёрский режим для типографий
Когда базовый конструктор заработал стабильно, проект оброс инструментами для бизнеса и печатников.
Корпоративный API и пакетная сборка из CSV
У розничных сетей, франшиз и агентств недвижимости задача обычно звучит так: «Открываем 40 точек, вот таблица с адресами и телефонами, нужно 40 готовых PDF». Для них сделано API с авторизацией по ключам (Bearer bp_live_...), где кроме одиночного рендера POST /api/v1/render и проверки квоты GET /api/v1/usage есть пакетная загрузка CSV (POST /api/v1/batch/submit): файл без заголовка до 500 строк, где каждая строка — отдельный баннер, а колонки — строки текста. Вся таблица валидируется до постановки в очередь; если в данных ошибка, сервер сразу возвращает 422 с перечнем конкретных строк и ячеек. Сборка идёт через пул процессов и отдаёт готовый ZIP-архив, причём квота по тарифу списывается только один раз — при первом успешном скачивании архива.
Промокоды с атомарным резервом
Промокоды поддерживают скидку от 1% до 100% и заданное число применений (например, 1, 3 или 5). Чтобы одноразовый код нельзя было применить параллельными запросами, списание сделано без предварительного SELECT — одним атомарным UPDATE promo_codes SET uses_left = uses_left - 1 WHERE code = ? AND uses_left > 0 AND active = 1 с проверкой rowcount.
Использование резервируется при создании заказа, но если платёж в ЮKassa не создался, был отменён или заказ провисел без оплаты больше часа, фоновая задача автоматически возвращает попытку обратно на баланс промокода.
Партнёрский режим для типографий без регистрации покупателя
В любую типографию приходят клиенты, которым нужен простой баннер «ПРОДАМ / АРЕНДА», но у которых нет макета. Держать дизайнера ради вёрстки трёх слов за 300 рублей печатникам невыгодно, а отказывать — терять заказ на печать.
Для таких случаев был сделан партнёрский режим (первая типография уже работает в продакшене). Типография ставит редирект со своего домена или кнопку на сайте на bannerbot.ru/?partner=<код>. Ссылка с автозаполнением размера баннера, текстовых полей и цветов, и при желании партнер может настраивать ее под разные задачи. Шапка конструктора окрашивается в цвета типографии: подставляются её логотип и кликабельный телефон (tel:), а ниже указывается, что оплату и техподдержку обеспечивает BannerBot. Клиент платит стандартную стоимость макета и скачивает PDF себе, а копия этого же файла автоматически уходит на почту типографии. За каждый оплаченный макет типографии начисляется согласованная сумма.
Одна из инженерных задач здесь — как связать клиента с заказом в типографии, не требуя от него регистрации, имени, почты или телефона в форме оплаты. Из sha256(order_id) берутся первые 8 hex-символов по модулю $10^6$ и форматируются в короткий шестизначный номер вида 482-915. Скачиваемый клиентом файл получает имя: Заказ 482-915, скажите его в Название по номеру 90-20-55.pdf. В ту же секунду в типографию приходит письмо с темой Заказ 482-915 и этим же PDF во вложении. Клиенту достаточно позвонить в типографию и продиктовать шесть цифр. Поскольку тексты заказов хранятся всего 14 дней, шестизначный код работает как короткий пин-код для активных заказов текущих двух недель, который удобно продиктовать голосом (при сотне заказов у одной типографии за это окно вероятность совпадения двух кодов составляет около 0,5%, и даже в этом случае менеджер сразу различит макеты по тексту баннера в письме; если объём заказов у партнёра вырастет на порядки, длину кода несложно увеличить).
Отдельная деталь всплыла при подключении партнёрского логотипа: один из SVG-файлов, экспортированных из CorelDRAW, оказался сохранён в кодировке UTF-16 (в диалоге экспорта Corel есть такой вариант). Браузеры на базе Chromium такой файл молча отображают, а другие парсеры и браузеры могут его не прочитать. Поэтому все локальные SVG-логотипы партнёров проверяются автотестом на чистый UTF-8 без BOM, валидный XML и отсутствие <script>, <foreignObject>, внешних ссылок и on*-атрибутов.
Эксплуатация: релей для Telegram, Web Push и отказ от сторонней аналитики
Двусторонний релей на Cloudflare Workers и PWA
Уведомления о заказах и кнопка принудительной выдачи PDF завязаны на Telegram-бота. После деплоя на российский VPS (hostland.ru) оказалось, что с хостинга не проходят соединения к api.telegram.org в обе стороны: исходящие уведомления падали по таймауту, а входящие нажатия inline-кнопок от Telegram не доходили до Nginx.
Чтобы не поднимать отдельный сервер с VPN ради сервисных сообщений, я вынес шлюз на бесплатный Cloudflare Worker (web/tools/cf-tg-relay.js). Для исходящих вызовов в переменной TG_API_BASE задан HTTPS-адрес воркера с секретным префиксом в пути (это было быстрое решение, позволившее не менять вызовы стандартного Bot API в коде, хотя с точки зрения строгой гигиены секрет лучше передавать в HTTP-заголовке, чтобы он не светился в логах самого воркера). В обратную сторону нажатия кнопок (callback_query) Telegram шлёт на воркер, а тот пересылает запрос на /api/tg/callback сервера, где проверяется заголовок X-Telegram-Bot-Api-Secret-Token.
Вторым, полностью независимым каналом сделан Web Push: админка оформлена как PWA и отправляет уведомления через браузерные push-сервисы (Mozilla, FCM, Apple) с помощью pywebpush и VAPID-ключей.
Собственная аналитика без кук и фильтр EasyPrivacy
Когда в сервисе появились платежи и партнёрский режим, я решил минимизировать сбор данных: удалил Яндекс.Метрику с Вебвизором, перенёс шрифты (.woff2 с лицензиями OFL) локально на сервер и сделал ленивую подгрузку SDK ЮKassa только на экране сверки заказа. Сайт избавился от сторонних кук и внешних сетевых запросов при старте.
Чтобы понимать, какие статьи приводят покупателей, я написал лёгкий скрипт счётчика без кук и идентификаторов пользователя, который пишет в SQLite только суточные агрегаты (день + событие + страница + категория источника). На уровне Nginx для этой ручки отключён access_log и обнуляются заголовки X-Real-IP и X-Forwarded-For.
Здесь случился показательный казус: сначала я назвал скрипт /stat.js, а эндпоинт — /api/stat. При проверке по спискам блокировщиков выяснилось, что в популярном листе EasyPrivacy (используется в uBlock Origin, AdGuard и браузерах со встроенной защитой) есть общее правило, блокирующее любой путь с подстрокой /stat.js?. Переименование файла в нейтральный /kit.js, а ручки — в POST /api/beat дало ноль совпадений по четырём основным базам фильтр-листов.
А чтобы воронка не рвалась после банковского редиректа (3DS / СБП), где document.referrer сбрасывается, события успешной оплаты (order_paid) и скачивания (file_downloaded) записывает сам бэкенд по метке входной страницы, сохранённой при создании заказа.

Что касается технических мер минимизации данных: текст макета, IP и User-Agent хранятся в БД ровно 14 дней (это покрывает сутки на бесплатную правку и 10 дней на рассмотрение возможных обращений по возврату), после чего стираются фоновым процессом purge_order_pii. В логах Nginx последний октет IP маскируется до подсети /24 с ротацией через 14 дней. Автотесты в CI следят за тем, чтобы сроки хранения в коде, конфиге logrotate и тексте политики конфиденциальности не расходились; при этом отдельная юридическая экспертиза на соответствие всем требованиям 152-ФЗ не проводилась — целью было именно не копить на сервере лишние данные пользователей.
Поисковый трафик и продуктовые выводы
Изначально хотелось нагенерировать по шаблону 200 посадочных страниц вроде «Баннер для шиномонтажа 3×2». Однако анализ реальных запросов в Яндекс.Вордстате (выгрузка за сентябрь–октябрь 2026 года по всей России) быстро скорректировал планы. Выяснилось, что высокочастотные запросы вроде «печать баннеров» и «купить баннер» (около 24 тыс. показов в месяц) вводят люди, которым нужно готовое физическое полотно с доставкой, а не PDF-макет. Реальный же спрос на подготовку макета сгруппирован вокруг конкретных габаритов («баннер 2 на 2» — около 3,6 тыс., «3 на 3» — около 3,4 тыс., «метр на метр» — около 1,7 тыс., «3 на 2» — около 1,4 тыс.) и прикладных вопросов: расчёта высоты букв по расстоянию, шага люверсов и выбора плотности ткани.
Тогда же решил отказаться от формата билбордов 3 х 6 м (6 000 мм). Во-первых, целевых запросов на макет 3 х 6 почти нет, так как щиты обычно заказывают через агентства. Во-вторых, при стандартном масштабе PDF 1.4 предел размера страницы в Adobe Acrobat составляет 200 дюймов (14400 pt = 5080 mm). В более новых версиях стандарта (с PDF 1.6) для крупных форматов можно задать масштабирующий коэффициент UserUnit, но усложнять генератор, инспектор pdf_check и проверку совместимости с программами типографий ради невостребованного размера не имело смысла.
В итоге вместо сотен однотипных страниц в проекте работает генератор seo/build_pages.py на 17 содержательных страниц со встроенными таблицами расчёта читаемости шрифта (эмпирический ориентир 1:300), расчётом люверсов и кнопками, которые открывают конструктор с уже предзаполненным размером, текстом и цветовой парой (/?size=2x1&text1=АРЕНДА...).
Итог
Попытка автоматизировать собственную рутину с «тремя словами на заборе» вылилась в интересный инженерный проект. Оказалось, что даже вокруг простого векторного прямоугольника со строками текста возникает целый пласт задач: пришлось учитывать особенности Ghostscript при переводе шрифтов в кривые и следить за совместимостью координат, разделять полиграфическую теорию офсетной печати и практику широкоформатных принтеров, программно проверять каждый собранный PDF и синхронизировать его с тем, что обещает сайт, проектировать оплату, токены и промокоды атомарно, оставляя покупателю право на опечатку и обрыв связи.
Проект работает в продакшене и экономит время мне, заказчикам и печатникам.
Код проекта: GitHub — Ale007XD/BannerPDF
Буду рад ответить на вопросы по препрессу, Ghostscript и архитектуре в комментариях!
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.