Чуть более месяца назад я устроился верстальщиком в старт-ап, в команду Ruby-разработчиков. Так повезло, что команда оказалась очень хорошей и моё стремление учиться совпало с их желанием получить хорошего специалиста.
HTML-вёрстка сама по себе имеет немного ценности и не единственное, чем можно нагрузить верстальщика.
На нашем сайте пользователь оформляет себе покупку и ему на почту уходит подтверждение с электронным билетом. в котором указаны детали заказа, а так как в хорошем проекте всё должно быть хорошо и ярко, дизайнер нарисовал макет квитанции. Ну а мне, как верстальщику было поручено реализовать это всё в коде.
Варианты генераторов для Ruby
Согласно сайту Ruby Toolbox существует два принципиальных подхода к генерации PDF-файлов:
- wkhtmltopdf и различные обёртки на него
- Prawn
Первый вариант подразумевает генерацию HTML-страницы и конвертацию её в PDF, в то время как второй позволяет, по факту, работать с canvas и генерировать документ без дополнительных прослоек.
Я выбрал вариант с использованием Prawn (по большей части, конечно, по тому, что предыдущая версия PDF-файла генерировалась этим способом) даже не смотря на то, что мне пришлось вынырнуть из привычного мне мира HTML и CSS
Тех, кому интересно приглашаю под хабракат.
Особенности работы с Prawn
Я не стану рассказывать, как подключить этот gem к проекту и настроить его — на хабре уже была аналогичная статья. Я расскажу про особенности вёрстки документов с использованием этого гема.
Настройки страницы
Первое, на что я наткнулся — формат бумаги. По умолчанию. для нового документа Prawn использует размер бумаги Letter.
Кроме того, есть возможность указать поля margin, фоновое изображение.
Так же стоит помнить, что работаем мы не с пикселями, а с типографскими пунктами.
img = "#{Prawn::DATADIR}/images/background.jpg"
Prawn::Document.generate('hello.pdf', :page_size => "A4", :margin => 20, :background => img) do |pdf|
pdf.text 'Hello World!'
end
Данный код генерирует документ с именем hello.pdf на листе размера A4, с полями по 20 пунктов, фоновым изображением background.jpg и текстом 'Hello World!'
Вывод текстовых блоков
Генератор поддерживает два типа вывода текста — text и text_box. В первом случае просто выводится строка текста в месте, где на данный момент установлен курсор. Во втором выводится контейнер с текстом, которому можно задать размеры через опции :width и :height, обтекание через опцию :overflow (принимающую значения :expand и :shrink_to_fit) границы и, главное, абсолютное положение через параметр :at.
Если в приведённом мною ранее коде заменить 'Hello World!' на 'Привет!' то мы резонно получим проблему со шрифтами.
В своём проекте мы используем проприоритарный шрифт Proxima Nova. Для того, чтобы генератор знал, какой мы хотим использовать шрифт и с какими стилями текста будем работать, необходимо явно указать шрифты.
font_families: {
proxima: {
bold: "assets/fonts/proximanova-bold-webfont.ttf",
normal: "assets/fonts/proximanova-reg-webfont.ttf",
light: "assets/fonts/ProximaNova-Light.ttf"
}
}
Prawn::Document.generate('hello.pdf') do |pdf|
pdf.font_families.update("Proxima Nova" => @opts[:font_families][:proxima])
pdf.font "Proxima Nova", size: 12, style: bold
pdf.text 'Hello World!'
end
Этот код выведет тот же самый текст, размером в 12 пунктов используя шрифт proximanova-bold-webfont.ttf.
Цвет шрифту задаётся через атрибут документа fill_color и может иметь шестнадцатеричное значение.
fill_color = "ffffff"
Кроме того, можно указать междустрочный интервал через default_leading или указав прямо в текстовом блоке параметр :leadig. Отступы между параграфами задаются через :indent_paragraphs.
При печати текстов зачастую используется неразрывный пробел. А так как мы работаем не с HTML — документом, где можно просто указать код то приходится идти на хитрости и подставлять специальный метод: Prawn::Text::NBSP.
Так же prawn понимает такие параметры, как :kerning и :character_spacing для кернинга и межбуквенного интервалов соответственно. Кернинг принимает либо true либо false, в то время, как character_spacing задаётся в пунктах.
default_leading 5
text string, :kerning => true, :character_spacing => 5
move_down 20
text string, :leading => 10, :indent_paragraphs => 60
move_down 20
text string + '#{Prawn::Text::NBSP * 10}' + string
Данный код устанавливает междустрочный интервал в 5 пунктов, выводит строку с кернингом и межбуквенным расстоянием в 5 пунктов, опускает курсор на 20 пунктов, выводит строку с междустрочным интервалом в 10 пунктов и расстоянием между параграфами в 60 пунктов. Спускает курсор ещё на 20 пунктов и выводит две строки, разделённые десятью неразрывными пробелами.
Из дополнительных возможностей при работе с текстом стоит упомянуть возможность вращения текста через параметр :rotate, который принимает в качестве значения угол в градусах. Так же можно использовать опциональный параметр :rotate_around для того, чтобы указать направление вращения (по умолчанию :upper_left) и возможность строчного форматирования в духе HTML:
text "Эта <font size='18'>строка</font> использует " + "<font name='Courier'>все атрибуты </font> тега font в " + "<font character_spacing='2'>одном месте</font>. ", :inline_format => true
Но так как моя задача состояла не в печати книги, а в выводе информации о заказе и электронного билета, сильно в подробности работы с текстом я не вдавался.
Позиционирование
В prawn элементы позиционируются либо относительно, начиная с верха документа путём спуска курсора вниз через move_down, либо же абсолютно. Именно с абсолютным позиционированием и возникает основная сложность, так как оно происходит не от верхнего левого угла, как можно было бы предположить, а от нижнего левого, как будто при построении графиков. Именно эта особенность, а так же то, что единицы измерения — пункты, а не привычные пиксели и доставила мне больше всего трудностей при вёрстке.
text_box 'test', :at[10,100]
Этот код выведет строку 'test' внизу страницы в 10 пунктах от левого поля страницы и в 100 пунктах от нижнего.
Графические примитивы
В макете, нарисованном нашим дизайнером присутствовало достаточно много графических элементов, которые не очень то хотелось вставлять изображениями. Именно для таких случаев в генераторе предусмотрена возможность работы с графическими примитивами — линиями(horizontal_line, vertical_line), окружностями(fill_circle и stroke_circle) и полигонами(fill_polygon и stroke_polygon) с заливкой и без.
Цвет заливки используется такой же, как и цвет текста и тоже устанавливается через fill_color, цвет контурных линий же указывается через stroke_color. Кроме того, можно указать ширину линий через параметр line_width
Вот пример функции, которая рисует круг с обводкой, линией по центру и указателем-треугольником
def draw_circle_part(colors, left, top, pdf)
pdf do
fill_color colors['circle_1']
fill_polygon [left['circles_left'], top['polygon_2_top']], [left['line_1'], top['polygon_1_top']], [left['line_2'], top['polygon_1_top']]
fill_color colors['circle_2']
fill_circle [left['circles_left'], top['circles_top']], 28
stroke_color colors['circle_3']
line_width 1.5
stroke_circle [left['circles_left'], top['circles_top']], 28
stroke_color colors['circle_4']
stroke do
horizontal_line left['line_1'], left['line_2'], :at => top['circles_top']
end
line_width 1
end
end
Так же, полезным может оказать возможность рисовать кривые и произвольные линии. Для этого используются методы stroke.line и stroke.curve для отрисовки линий и кривых из одной указанной точки в другую, а так же stroke.line_to и stroke.curve_to для линий из текущего положения курсора в точку. При том, у кривых можно задать параметр :bounds, указывающий точки через которые будет проходить кривая. При том для построения будет применяться преобразование Безье.
stroke do
line [300,200], [400,50]
curve [500, 0], [400, 200], :bounds => [[600, 300], [300, 390]]
end
Изображения
При работе с изображениями стоит очень внимательно относиться к размерам и помнить, что лучше подготовить изображение на 200% большее, чем в макете и затем задать в prawn явно размеры, позволив генератору уменьшать изображение, чем отдавать точно такое же как в макете.
По личному опыту, когда я вставлял иконки для блоков деталей заказа и использовал вырезанные прямо из макета изображения с оригинальными размерами, я получал замыленные границы как при неудачном увеличении изображения. Эмпирическим путём для себя установил идеальное соотношение вставляемого изображения к исходному как 2 к 1. Благо все объекты в макете были отрисованы как графические примитивы и проблем с изменениями размеров не возникло.
Изображение по умолчанию имеет оригинальный размер и помещается в точку, где установлен курсор. Для абсолютного позиционирования используется параметр :at. Относительно же позиционировать изображение можно через параметр :position, который принимает значения :left, :center, :right или же число пунктов от левой границы и параметр :vposition, принимающий значения :top, :center, :bottom или отступ от нижней границы в пунктах.
Высоту и ширину изображения можно задать через :height и :width соответственно. При том, если указан только один параметр, второй будет подобран автоматически с сохранением пропорций. Аналогично можно указать не точные размеры а пропорциональное изменение изображения через :scale.
image "assets/images/details.png", :at => [25, 641], :height => 22
image "assets/images/prawn.png", :scale => 0.7, :position => :right, :vposition => 100
Таблицы
Практически невозможно сверстать квитанцию не используя таблиц. Для создания таблиц prawn предусматривает два метода: table и make_table. Оба метода создают таблицу с той лишь разницей, что table вызывает метод отрисовки сразу после создания таблицы, в то время как make_table всего лишь возвращать созданную таблицу.
Самый удобный способ создания таблицы — это передача в метод массива массивов данных, гд каждый внутренний массив представляет собой одну строку. Если передать в массиве объект, созданный через make_table будет создана таблица внутри таблицы.
Так же в таблицу можно передавать хэши с ключами :image для изображений и :content для вставки форматированного текста.
cell_1 = make_cell(:content => "this row content comes directly ")
cell_2 = make_cell(:content => "from cell objects")
two_dimensional_array = [ ["..."], ["subtable from an array"], ["..."] ]
my_table = make_table([ ["..."], ["subtable from another table"], ["..."] ])
image_path = "#{Prawn::DATADIR}/images/stef.jpg"
table([ ["just a regular row", "", "", "blah blah blah"],
[cell_1, cell_2, "", ""],
["", "", two_dimensional_array, ""],
["just another regular row", "", "", ""],
[{:image => image_path}, "", my_table, ""]])
Данный код выведет вот такую таблицу (пример из документации):
Для таблицы можно указать следующие опции:
- :position — по аналогии с изображением, при относительном позиционировании выравнивает таблицу либо по краям, либо по центру, либо с отступом от левой границы документа
- :column_widths — принимает либо массив с размерами для каждой колонки, либо объект в котором ключ это номер колонки, а значение — ширина (например {2 => 240})
- :width — ширина таблицы. По умолчанию, таблица имеет ширину контента
- :row_colors — массив цветов для строк. В случае, если в массиве меньше цветов, чем строк в таблице, цвета будут браться из массива циклически.
Кроме того, можно задавать параметры для ячеек:
- :padding — по аналогии с CSS задает отступы контента от границ ячеек и, как и в CSS параметры идут в порядке [верхний отступ, правый отступ, нижний отступ, левый отступ]
- :borders — массив границ, которые будут установлены у ячейки. По умолчанию — все границы видны.
Это далеко не полный список свойств таблиц, но их хватит для знакомства с таблицами в prawn и для решения большинства прикладных задач.
Заключение
По итогам работы с данным генератором могу сказать следующее — для моей конкретной задачи Prawn, даже не смотря на то, что код, который требуется набрать для генерации выглядит весьма громоздко, подошел практически идеально, так как сама квитанция не имеет роута в проекте, и генерируется из набора данных в формате JSON, полученных от backend'a.
Лично мне было удобно выносить повторяющиеся блоки prawn-кода в отдельные функции и просто вызывать их в нужных местах, использовать Ruby-код для разбора пришедшего набора данных, итераций по объектам, что весьма проблематично сделать на HTML.
Пример получившегося у меня документа с данными на двух пассажиров по сложным маршрутам можно скачать тут: Электронный билет
Однако, если требуется просто предоставить pdf-версию уже существующей на сайте страницы, то проще и выгоднее использовать PDFKit, который умеет создавать PDF-файлы напрямую из указанной ему HTML-страницы.
Автор: pman