Решили издать каталог подразделений, проектов и людей достаточно крупной организации — и встал вопрос, в чем именно готовить макет: InDesign, TeX или typst. При выборе инструмента хотелось учесть:
1) удобство работы каждого участника,
2) удобство совместной работы,
3) удобство внесения правок в последний момент.
Третий пункт даже был самым важным, посколько было очевидно, что первоначальные данные весьма грязные, будут правки не только орфографические, но и в масштабе плюс/минус подраздел.
InDesign — старый добрый друг, в котором есть работа со стилями, скрипты типа DoTextOK, генерация оглавлений и прочее. Но я пока не освоил систему совместной работы в InDesign, плюс планировалась верстка с выводом данных сеткой — а это означало бы, что при добавлении или удалении одного элемента немалую часть работы пришлось бы делать заново (опасения о таких подковырках оправдались). Пугала еще одна рутинная процедура — вставка вручную QR-кодов, которых в итоге оказалось 170 штук. Напутать их было бы проще простого.
TeX для меня — тоже старый добрый друг, о сопоставлении его с InDesign я уже писал на Хабре. Совместную работу можно было организовать в git, устойчивость к внесению правок в последний момент вполне надежная. QR-коды умеет генерировать сам.
Но на горизонте появился typst, который работает по принципу TeX'а (гибрид текста и команд + картинки --> pdf), но в несколько раз быстрее (особенно если TeX надо прогонять несколько раз для выставления перекрестных ссылок). Еще больше порадовал typst тем, что можно прямо в режиме реального времени видеть результат всех вносимых изменений (Visual Studio Code + плагины для typst), а также прыгать из превьюшки на нужное место кода, а из кода — на нужное место превьюшки.
И окончательное решение в сторону typst было принято, когда стало понятно, что в нем можно удобно представить исходные данные по типу JSON, конкретно у меня — как список словарей
#let names = ((name: "Иван", year: "2000"), (name: "Петр", year: "2010"))
а далее сортировать, фильтровать, заниматься арифметическими вычислениями. Ну и он умеет делать QR-коды.
Итак, был выбран typst, а для изящной типографики (и опять же, для избавления от прогонки скриптами) вот такая постепенно сформировалась преамбула.
Размеры страницы, поля, нижний колонтитул с выравниванием по внешнему краю. У нас не было картинок с вылетом за обрез, так что типография приняла макет в обрезном формате, но в typst можно задать и обрезной отступ параметром outset.
#set page( width: 170mm, height: 225mm, margin: (inside: 21mm, outside: 14mm, top: 19mm, bottom: 28mm), numbering: "1", footer: context{ set text(size: 16pt, number-type: "old-style", number-width: "proportional") let (n,) = counter(page).get() set align(if calc.even(n) { left } else { right }) counter(page).display("1") } )
Основной шрифт задан был так:
#set text(font: "Ladoga", lang: "ru", size: 12pt, fill: cmyk(0%,0%,0%,100%))
Последняя строка появилась уже при проверке макета в Adobe Acrobat Pro: оказалось, что основной текст идет не чистым черным, а составным из 4 красок — на экране это, конечно, не заметишь, а вот при офсетной печати в типографии на такой подход ругались бы знатно.
С русским языком typst работает хорошо, неадекватных переносов замечено не было (в отличие от InDesign, в который правильно ставить модуль переносов Батова).
Абзацы равняем по ширине, выставляем интерлиньяж (spacing) и абзацный отступ (я сторонник убирать «красную строку» после заголовков, так что all: false).
#set par(justify: true, spacing: 0.65em, first-line-indent: (amount: 1em, all: false), )
Дальше идет ряд директив, указывающих, как обращаться со знаками процента, номера, однобуквенными словами (считаю, что лепить их к следующему слову надо только в случае заглавной буквы). Тире не должно отрываться от предыдущего слова, а отступы вокруг него хочется сжать до 30%. Сокращения в адресах не должны отрываться от имен населенных пунктов и от чисел.
// знак процента #let percents = regex("(\d+)\s*\%") #show percents: it => { let (d, ) = it.text.match(percents).captures [#box([#d\u{2009}%])] } // знак номера #let numero = regex("№\s*(\d)") #show numero: it => { let (d, ) = it.text.match(numero).captures [№\u{2009}#d] } // однобуквенными слова в начале предложения #let aviko = regex("(\b[АВИКОСУЯ])\s+") #show aviko: it => { let (d, ) = it.text.match(aviko).captures [#d~] } // инициалы и фамилия #let fio = regex("(\b[А-ЯЁ]\.)\s*([А-ЯЁ]\.)\s*([А-ЯЁ][а-яё])") #show fio: it => { let (i, o, f) = it.text.match(fio).captures [#i\u{202F}#o\u{202F}#f] } // тире #let tire = regex("\s+(—)\s+") #show tire: it => { let (d, ) = it.text.match(tire).captures [#text(spacing: 30%)[~#d ]] } // сокращения в адресах #let gorod_selo = regex("\b((г|с|пос|дер|д|ул|пер|п|кв)\.)\s+") #show gorod_selo: it => { let d = it.text.match(gorod_selo).captures.first() [#d~] }
Отдельная заморочка была с телефонами. Убираем все лишние символы, определяем типичные 5-циферные коды регионов, выводим красиво и единообразно, без скобок, с пробелами и маленькими центральными точками, отделяющими последние две пары цифр, типа +7 910 123⋅45⋅67. Местные номера — по типу +7 48532 1⋅23⋅45. Если эстетическое чувство захочет изменить подачу, меняем всё несколькими нажатиями клавиш.
#let format_phone(p) = { let psplit = p.split(" доб. ") if psplit.len() > 1 { return format_phone(psplit.at(0)) + " доб." + sym.space.nobreak.narrow + psplit.at(1) } let digits = p.replace(regex("[^0-9]"), "") if digits.len() != 11 {return p} let formatted = "+7" let space = " " let space_nobreak = sym.space.nobreak // let space = sym.space.narrow // let space_nobreak = sym.space.nobreak.narrow let open_bracket = "" let close_bracket = "" // let open_bracket = "(" // let close_bracket = ")" // let separator = "-" let separator = sym.space.hair + sym.dot.op + sym.space.hair if digits.slice(1,3) == "48" { formatted += space_nobreak + open_bracket + digits.slice(1,6) + close_bracket + space + digits.at(6) + separator + digits.slice(7,9) + separator + digits.slice(9,11)} else { formatted += space_nobreak + open_bracket + digits.slice(1,4) + close_bracket + space + digits.slice(4,7) + separator + digits.slice(7,9) + separator + digits.slice(9,11)} return formatted }
С форматированием электронных адресов всё проще, просто убираем заглавные:
#let format_email(e) = {lower(e)}
Счастье, которое было у меня от генерации QR-кодов командами qrcode("habr.com", 2cm), почти рухнуло на этапе препресса �� Adobe Acrobat выдал ошибку, что все QR-коды не чисто чёрные, а 4-цветные. Прописывание cmyk-цвета в качестве параметра вызова не приносило нужного результата. Связано это было с тем, что генерация QR-кода происходит через промежуточный этап в SVG, а формат SVG хранит информацию о цвете только RGB, но не CMYK. Тыканье мэтров на форумах не принесло результатов. Пришлось самому написать код, который разбирает текст SVG и отрисовывает. Оказалось не слишком сложно. Там QR-код записан как набор прямоугольников, заданных командами типа "M1 2h3v4h-3Z": сдвинься в точку (1,2), рисуй направо 3 клетки, вверх 4 клетки, налево 3 клетки, замкни контур. Парсим, получаем массив координат и размеров, рисуем. Правда, это решение было придумано на следующий день после того, как макет ушел в печать — быстрее оказалось прощелкать все 170 QR кодов в Acrobat'е и преобразовать их в чистый CMYK-черный.
#let black_cmyk_qrcode(text, width: 1.5cm, color: cmyk(0%, 0%, 0%, 100%)) = { let svg-bytes = qrcode(text).source let qr_path = str(svg-bytes).match(regex("<path d=\"([^\"]*)")).captures.at(0) // get the path part of code from SVG let qr_rectangles_text = qr_path.matches(regex("M(\d+) (\d+)h(\d+)v(\d+)h-\d+Z")) let qr_rectangles_coord = () for qr in qr_rectangles_text { qr_rectangles_coord.push(("x", "y", "width", "height").zip(qr.captures.map(x => int(x))).to-dict()) } let tiles = calc.max(..qr_rectangles_coord.map(x => (x.x + x.width))) let tile_size = width/tiles rect(width:width, height: width, inset: 0mm, stroke: 0pt, { for r in qr_rectangles_coord { place(dx: r.x*tile_size, dy: r.y*tile_size, rect(height: r.height*tile_size, width: r.width*tile_size, fill: color)) }} ) }
Итоговый макет на 240 страниц содержал 480 картинок, 170 QR-кодов и весил 2.2Gb. С таким размером он уже переставал показывать превью в Visual Studio Code.
Сжатый вариант макета весом 6Mb можно посмотреть/скачать тут.
Главный вывод, который я сделал для себя — typst удобен, быстр и перспективен, буду дальше пользовать его и для книг, и для рутины с договорами, и для разминки мозгов.
Комментарии (18)

aborouhin
18.02.2026 09:06typst выглядит интересно, ну и open source - вообще замечательно. Надо посмотреть подробнее.
Но для поставленной задачи, особенно если пользователь не особо программист, вроде как, идеально подошёл бы софт для technical writing типа Adobe FrameMaker или MadCap Flare (да, ценник, ну так и InDesign не бесплатный...) Не смотрели в эту сторону?

pantlmn Автор
18.02.2026 09:06Пока что не смотрел, глянул сейчас на краткие презентации.
Фишка как раз в том, что пользователь (я) в данном случае — программист, и мне ближе такая концепция, что есть исходник со всякими функциями, из которого компилируется итог, а не большая конструкция, по которой прогоняются скрипты (насколько я понимаю, у Adobe именно такая логика). Если расстановка неразрывных пробелов происходит скриптом, то его надо прогонять всякий раз после редакторских изменений. А если правила изначальна записаны в regexp, которые гоняются при компиляции — то про это никто не должен помнить, и соответственно, никто не забудет.
aborouhin
18.02.2026 09:06Ну там общая логика в разделении содержания (которое в идеале в XML-формате семантической разметки - DocBook, DITA или отраслевые стандарты) и представления. Как раз заточено на толстенные фолианты с кучей сложных нумераций, табличек, индексов, оглавлений и пр., которые верстаются автоматом на основе шаблона и стилей. При этом шаблон и стили редактируются в интерфейсе, доступном обычному верстальщику (ну по крайней мере в FrameMaker и в те годы, когда я им пользовался, - давно уже у меня не было таких задач).
P.S. Но вот вся эта часть с редактурой исходного текста на основе регекспов - это да, про другое. Я бы этот процесс от собственно вёрстки отделял. Всяких скриптов для таких задач в формате от макросов Word до веб-сервисов достаточно, да и свой написать просто. Просто прогоняем этот "препроцессор" перед импортом текста.

pantlmn Автор
18.02.2026 09:06Логика понятная, ее несложно реализовать и в typst: вот данные, вот код, который их правильно парсит. Собственно, я так и делал. Вот данные, я их правильно сортирую (такого нет в Adobe), по-умному модифицирую и вывожу, лишь однажды заморочившись с правилами, касающимися однобуквенных слов [АВИКОСУЯ] и прилепляния тире.
И проблема с тем, что в FrameMaker QR-код невозможно сразу сделать 100% black в CMYK, точно вылезет, поскольку FM их генерирует и сохраняет в PNG:
https://help.adobe.com/en_US/framemaker/using/using-framemaker/user-guide/frm_graphics_gr-topic_graphics-qrcodes.html
aborouhin
18.02.2026 09:06Я глянул доки на typst поподробнее - там как-то очень грустно именно с семантической разметкой. Один heading, один list и, если я правильно понял, вообще никакого аналога стилей и всяких блоков/сносок/выносок/перекрёстных ссылок и пр. - как с этим жить-то? Так что если уж надо подтянуть MarkDown до пригодного к употреблению уровня - я бы всё же смотрел на AsciiDoc. Ну а если у нас энтерпрайз и всё серьёзно - то на ту самую DITA.
P.S. Посмотрел доки подробнее - то, что я написал выше, в каком-то объёме в typst есть, но очень уж странно оно как-то организовано, надо понять философию сначала.

pantlmn Автор
18.02.2026 09:06Там это всё реализовано функциями, примерно как в ТеХе. Задал всё что нужно, и вызвал напрямую, либо задал, что она сработает на основе regexp'ов.
Со сносками и перекрёстными ссылками тоже всё в порядке.

aborouhin
18.02.2026 09:06Да я уже посмотрел, именно поэтому и удивился в P.S. :) Но всё равно непонятно, как структурировать текст изначально с таким скудным набором семантических элементов. Ну вот сравните с элементами DITA... да хотя бы даже с HTML5. А тут на каждый документ придётся какую-то свою схему через свойства колхозить, что ли.

pantlmn Автор
18.02.2026 09:06Наверное, можно спокойно DITA и другие xml-схемы использовать, просто рассказать typst, как их интерпретировать. Читать xml typst умеет.

aborouhin
18.02.2026 09:06Вот этого я и не понимаю. В моём мире есть:
документ на некоем языке разметки, в котором есть контент, но нет оформления и логики (ну или есть минимальная, типа условного текста) - XML со своей схемой (в т.ч. DITA), HTML5 с чисто семантическими тегами, даже документ Word без локального (отличного от шаблона) форматирования (хотя в Word для поддержания разделения контента и оформления встроенных средств нет) и т.п.
есть шаблон, в котором есть оформление и стили, но нет контента, - шаблон в том же FrameMaker или в Word, или CSS, или XSLT для формирования XSL-FO (хотя эта вундервафля оказалась настолько тяжёлой, что не взлетела :)
и есть софт для обработки документа, применения к нему шаблона и создания из него PDF, HTML и т.п.; тут же может быть логика какого-то более сложного редактирования текста, если она нам нужна.
А тут ни рыба, ни мясо, а всё вперемешку... Но я так понимаю, тут идеология растёт ногами из TeX, а я с ним никогда толком не работал.

pantlmn Автор
18.02.2026 09:06Думаю, Вам интересно будет, что ещё в typst я делал договоры: в одних файлах персональные данные, в других — списки обязанностей, в третьих — возможности видоизменять текст в соответствии с полом сотрудника(цы). Где нужно — инициалы автоматом делаются вместо имени и отчества.

aborouhin
18.02.2026 09:06Договоры мне в данный момент времени вообще наиболее интересны, я их автоматизацией занимаюсь :)
Сценарий Ваш понял, любопытно, но в целом не особо чего добавляет к тому же docassemble (там используют плейсхолдеры и jinja2 внутри Markdown / docx) или аналогичным решениям, а кривая обучения пользователя (которым в итоге будет юрист) покруче.

aborouhin
18.02.2026 09:06QR-код невозможно сразу сделать 100% black в CMYK,
Так это опять отдельный этап :) В экосистеме Adobe этим должен заниматься Acrobat (где-то там в функции Preflight можно задать профили преобразования цветов, которые глобально по всему документу поменяют CMYK на истинный чёрный)
AoD314
На сколько быстро собирается весь документ?
pantlmn Автор
Около 30 секунд на MacBook Air M4