Pull to refresh

Генерация PDF на сервере в Ruby

Reading time8 min
Views4.6K
Чуть более месяца назад я устроился верстальщиком в старт-ап, в команду Ruby-разработчиков. Так повезло, что команда оказалась очень хорошей и моё стремление учиться совпало с их желанием получить хорошего специалиста.

HTML-вёрстка сама по себе имеет немного ценности и не единственное, чем можно нагрузить верстальщика.

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

Варианты генераторов для Ruby


Согласно сайту Ruby Toolbox существует два принципиальных подхода к генерации PDF-файлов:


Первый вариант подразумевает генерацию 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-страницы.
Tags:
Hubs:
+15
Comments6

Articles