📜 ⬆️ ⬇️

PDF generation on the server in Ruby

Just over a month ago, I got a job as a layout designer at the start-up team of the Ruby-developers. So lucky that the team was very good and my desire to learn coincided with their desire to get a good specialist.

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

On our site, the user makes a purchase and a confirmation email with an e-ticket is sent to him by mail. in which the details of the order are indicated, and since everything must be good and bright in a good project, the designer drew a mockup for the receipt. Well, I, as a coder, were instructed to implement this all in code.
')

Generator Options for Ruby


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


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

I chose the option using Prawn (mostly, of course, due to the fact that 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 I was used to

Those who are interested invite under habrakat.

Features of working with Prawn


I will not begin to tell how to connect this gem to the project and to configure it - on Habré there was already a similar article . I will tell you about the features of the layout of documents using this heme.

Page settings

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

In addition, it is possible to specify margins margin, 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 with the name hello.pdf on an A4 size sheet, with fields of 20 points, background image background.jpg and the text 'Hello World!'

Display text blocks

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 positioned. In the second, a container with text is displayed, which can be sized using options : width and : height , wrapping through option : overflow (taking values : expand and : shrink_to_fit ) borders and, most importantly, absolute position through parameter : at .

If in the code I gave earlier replace 'Hello World!' on 'Hi, Habr!' then we reasonably get the problem with the fonts.

In our project we use the proprietary font Proxima Nova. In order for the generator to know what type of font we want to use and what text styles we will work with, it is necessary to 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 specified in the document attribute fill_color and can have a hexadecimal value.

 fill_color = "ffffff" 

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

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

Also prawn understands such parameters as : kerning and : character_spacing for kerning and letter spacing, respectively. Kerning accepts either true or false , while character_spacing is specified 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 string with kerning and a letter spacing of 5 points, lowers the cursor by 20 points, displays a line with a line spacing of 10 points and a distance of 60 points between paragraphs. Moves the cursor another 20 points and displays two lines separated by ten non-breaking spaces.

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

 text " <font size='18'></font>  " + "<font name='Courier'>  </font>  font  " + "<font character_spacing='2'> </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 didn’t go into much detail 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 comes not from the upper left corner, as one might assume, but from the lower left, as if plotting. It is this feature, as well as the fact that the units of measurement are points, and not the usual pixels, and gave me the most difficulty in layout.

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

This code will display 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 quite a lot of graphic elements that we didn’t really want to embed with images. For such cases, 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 also indicated by stroke_color . In addition, you can specify the width of lines using 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 

It can also be helpful to draw curves and arbitrary lines. For this, the stroke.line and stroke.curve methods are used 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, for curves you can set the parameter : bounds , which indicates the points through which the curve will pass. Moreover, the Bezier transform will be used for the 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 size and remember that it is better to prepare an image 200% larger than the layout and then explicitly set the size in the prawn, allowing the generator to reduce the image than to give exactly the same as in the layout.

From personal experience, when I inserted icons for blocks of order details and used images that were cut straight from the layout with original dimensions, I received blurred borders as if the image was not enlarged. Empirically, 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 size changes.

The default image has the original size and is placed at the point where the cursor is positioned. For absolute positioning the parameter is used : at . Regarding the same image can be positioned through the parameter : position , which takes values : left ,: center,: right or the number of points from the left border and the parameter : vposition , which takes values : top ,: center,: bottom or indent from the lower border in points .

The height and width of the image can be set via : height and : width, respectively. However, if only one parameter is specified, the second one will be selected automatically with the proportions preserved. Similarly, you can specify not the exact size but proportional change of 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 impose a receipt without using tables. To create tables prawn provides two methods: table and make_table . Both methods create a table with the only difference that table calls the draw method immediately after creating the table, while make_table just returns the created table.
The most convenient way to create a table is to pass an array of data to the method, where each internal array is a single row. If you pass an object created by make_table in an array, a table will be created inside the table.
Also in the table you can transfer hashes with keys: image for images and : content to insert 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 the following table (example from the documentation):

For the table, you can specify the following options:

In addition, you can set the parameters for the cells:

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

Conclusion


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

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

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

Source: https://habr.com/ru/post/157199/


All Articles