ESPN DeportesCon gol de Luis Suárez, Inter Miami abre el cerrojo de CincinnatiThe Jerusalem PostA different flamenco perspective comes to Tel AvivPunchGroup pledges massive votes for TinubuCNN TürkBakan Çiftçi'den 29 Ekim uyarısı! 4 ilde turuncu alarmESPNEvery Premier League team ranked by bang-for-buck: What if they spent like Man City?InquirerAuthorities seize P2.2M in contraband cigarettes in Sultan Kudarat townZDF heuteEntdecken Sie das ZDF-Nachrichtenstudio한겨레경기 버스, 30일 총파업 코앞…도, 전세버스·전철 ‘비상수송대책’ColliderNetflix’s Most-Watched Series Ever Officially Won’t Return in 2026UOLDeputados pedem investigação de listas de boicote e agressões em SC: Não estamos no nazismoSBS 뉴스장동혁 "선관위·특검, 야반도주하듯 투표함 들고 사라져…부정선거 증거 쏟아져"Global News‘Indestructible’ Downtown Eastside toilet suddenly out of service
The Daily Newsstand · Free, Always
Thursday, October 29, 2026

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

Translate

Я графический дизайнер на фрилансе. За полноценный макет — бренд-волл, сложную афишу, баннер с графикой и фотомонтажом — средний ценник начинается от 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) записывает сам бэкенд по метке входной страницы, сохранённой при создании заказа.

Скриншот внутренней страницы /admin/stats/ с агрегированной воронкой событий

Скриншот внутренней страницы /admin/stats/ с агрегированной воронкой событий

Что касается технических мер минимизации данных: текст макета, 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 и синхронизировать его с тем, что обещает сайт, проектировать оплату, токены и промокоды атомарно, оставляя покупателю право на опечатку и обрыв связи.

Проект работает в продакшене и экономит время мне, заказчикам и печатникам.

Буду рад ответить на вопросы по препрессу, Ghostscript и архитектуре в комментариях!

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.