Generating PDF on a server in Ruby

A little more than a month ago, I got a layout designer in a start-up, in a team of Ruby developers. So lucky that the team was very good and my desire to study coincided with their desire to get a good specialist.

HTML layout itself has a little value and is not the only thing that can load the layout designer.

On our website, the user makes a purchase for himself and a confirmation with an electronic ticket is sent to his mail. in which the details of the order are indicated, and since everything should be good and bright in a good project, the designer drew a layout for the receipt. Well, I, as a layout designer, was instructed to implement all this in code.

Generator Options for Ruby


According to the Ruby Toolbox website, there are two fundamental approaches to generating PDF files:


The first option involves generating an HTML page and converting it to PDF, while the second allows, in fact, working with canvas and generating a document without additional layers.

I chose the option using Prawn (for the most part, of course, because the previous version of the PDF file was generated in this way) even though I had to emerge from the world of HTML and CSS that I was familiar with

Those who are interested in inviting me under the habracat .

Features of working with Prawn


I'm not going to tell you how to connect this gem to the project and configure it - Habré had already had a similar article . I will talk about the features of document layout using this gem.

Page Settings

The first thing I came across was the paper size. Default. Prawn uses Letter paper size for the new document.

In addition, it is possible to specify margin fields, a background image.

It is also worth remembering that we are not working with pixels, but with typographic items.

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

This code generates a document called hello.pdf on an A4-sized sheet with 20-point margins, a background image background.jpg and the text 'Hello World!'

Text block output

The generator supports two types of text output - text and text_box . In the first case, a line of text is simply displayed in the place where the cursor is currently set. In the second, a container with text is displayed, which you can set dimensions through the options : width and : height , wrap through the option : overflow (accepting the values : expand and : shrink_to_fit ), the borders and, most importantly, the absolute position through the parameter : at .

If in the code I quoted earlier replace 'Hello World!' to 'Hello Habr!' then we reasonably get a problem with fonts.

In our project, we use the proprietary font Proxima Nova. In order for the generator to know which font we want to use and what text styles we will work with, we must explicitly specify the fonts.

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

This code will output the same text, 12 points in size, using the font proximanova-bold-webfont.ttf.

The font color is set through the document attribute fill_color and can have a hexadecimal value.

fill_color = "ffffff"

In addition, you can specify line spacing via default_leading or by specifying the leadig parameter directly in the text block . Indentation between paragraphs is specified through : indent_paragraphs .

When printing texts, an inextricable space is often used. And since we are not working with an HTML document, where you can simply specify the code, you have to go to the tricks and substitute a special method: Prawn :: Text :: NBSP .

Prawn also understands such parameters as : kerning and : character_spacing for kerning and letter spacing, respectively. Kerning accepts either true or false , whilecharacter_spacing is set in points.

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

This code sets the line spacing to 5 points, displays a line with kerning and letter spacing of 5 points, lowers the cursor by 20 points, displays a line with line spacing of 10 points and a paragraph spacing of 60 points. Moves the cursor down another 20 points and displays two lines separated by ten inextricable spaces.

Of the additional features when working with text, it is worth mentioning the ability to rotate text through a parameter : rotate , which takes an angle in degrees as a value. You can also use the optional parameter : rotate_around to indicate the direction of rotation (default : upper_left ) and the possibility of line formatting in the spirit of HTML:

text "Эта строка использует " + "все атрибуты  тега font в " + "одном месте. ", :inline_format => true

But since my task was not to print the book, but to display information about the order and the electronic ticket, I did not go into the details of working with the text.

Positioning

In prawn, elements are positioned either relatively, starting from the top of the document by moving the cursor down through move_down , or absolutely. It is with absolute positioning that the main difficulty arises, since it does not come from the upper left corner, as one might assume, but from the lower left, as if in the construction of graphs. It is this feature, as well as the fact that the units of measurement are points, and not the usual pixels, that gave me the most difficulties in layout.

text_box 'test', :at[10,100]

This code will output the string 'test' at the bottom of the page 10 points from the left margin of the page and 100 points from the bottom.

Graphic primitives

In the layout drawn by our designer there were a lot of graphic elements that I didn’t really want to embed with images. It is for such cases that the generator provides the ability to work with graphic primitives - lines ( horizontal_line , vertical_line ), circles ( fill_circle and stroke_circle ) and polygons ( fill_polygon and stroke_polygon ) with and without fill.

The fill color is used the same as the text color and is also set via fill_color , the color of the contour lines is specified through stroke_color . In addition, you can specify the line width through the line_width parameter

Here is an example of a function that draws a circle with a stroke, a center line and a triangle pointer

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

Also, it may be useful to be able to draw curves and arbitrary lines. To do this, use the stroke.line and stroke.curve methods to draw lines and curves from one specified point to another, as well as stroke.line_to and stroke.curve_to for lines from the current cursor position to a point. Moreover, you can set the parameter for the curves : bounds , indicating the points through which the curve will pass. Moreover, the Bezier transform will be used for construction.

stroke do
  line [300,200], [400,50]
  curve [500, 0], [400, 200], :bounds => [[600, 300], [300, 390]]
end

Images

When working with images, you should be very careful about the sizes and remember that it is better to prepare the image 200% larger than in the layout and then set the dimensions explicitly in prawn, allowing the generator to reduce the image than to render exactly the same as in the layout.

From personal experience, when I inserted icons for blocks of order details and used images cut directly from the layout with original sizes, I got blurred borders as if the image was not enlarged. Empirically, for myself, I set the ideal ratio of the inserted image to the original as 2 to 1. Fortunately, all the objects in the layout were drawn as graphic primitives and there were no problems with resizing.

Изображение по умолчанию имеет оригинальный размер и помещается в точку, где установлен курсор. Для абсолютного позиционирования используется параметр :at. Относительно же позиционировать изображение можно через параметр :position, который принимает значения :left, :center, :right или же число пунктов от левой границы и параметр :vposition, принимающий значения :top, :center, :bottom или отступ от нижней границы в пунктах.

Высоту и ширину изображения можно задать через :height и :widthrespectively. Moreover, if only one parameter is specified, the second will be automatically selected with the proportions preserved. Similarly, you can specify not the exact dimensions but the proportional change in the image through : scale .

image "assets/images/details.png", :at => [25, 641], :height => 22
image "assets/images/prawn.png", :scale => 0.7, :position => :right, :vposition => 100

Tables

It is almost impossible to make up a receipt without using tables. Prawn provides two methods for creating tables: table and make_table . Both methods create a table with the only difference that table calls the rendering method immediately after creating the table, while make_table only returns the created table.
The most convenient way to create a table is to pass an array of data arrays to the method, where each internal array is a single row. If you pass an object created through make_table to the array, a table will be created inside the table.
It is also possible to transfer hashes to the table with the keys: image for images and : content for inserting formatted text.

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, ""]])

This code will output such a table (example from the documentation):

You can specify the following options for the table:
  • : position - similar to the image, with relative positioning, aligns the table either along the edges, or in the center, or indented from the left border of the document
  • : column_widths - accepts either an array with dimensions for each column, or an object in which the key is the column number, and the value is the width (for example {2 => 240})
  • : width - the width of the table. By default, the table has a width of content
  • : row_colors - an array of colors for the rows. If the array has fewer colors than the rows in the table, the colors will be taken from the array cyclically.

In addition, you can set parameters for cells:
  • : padding - by analogy with CSS, it sets the indentation of the content from the borders of the cells and, like in CSS, the parameters go in the order [upper indent, right indent, lower indent, left indent]
  • : borders - an array of borders that will be set at the cell. By default, all borders are visible.

This is not a complete list of table properties, but they are enough to familiarize yourself with tables in prawn and to solve most applied problems.

Conclusion


Based on the results of working with this generator, I can say the following - for my specific Prawn task, even though the code that I need to type for the generation looks very cumbersome, it came up almost perfectly, since the receipt itself does not have a route in the project, and is generated from a JSON data set received from the backend.
Personally, it was convenient for me to make repeating blocks of prawn code into separate functions and simply call them in the right places, use Ruby code to parse the data set that came in, iterate through the objects, which is very problematic to do in HTML.

An example of a document that I got with data for two passengers on difficult routes can be downloaded here: E-ticket

However, if you just want to provide a pdf version of an existing page on the site, it’s easier and more profitable to use PDFKit , which can create PDF files directly from the specified HTML page.

Also popular now: