Статьи

Создание PDF-файлов из HTML с помощью Rails

Существует много способов создания PDF-файлов в Ruby и Rails. Скорее всего, вы уже знакомы с HTML и CSS, поэтому мы собираемся использовать PDFKit для генерации PDF-файлов с использованием HTML из стандартного представления Rails и кода стилей.

Внутренне, PDFKit использует wkhtmltopdf (WebKit HTML to PDF), движок, который будет принимать HTML и CSS, визуализировать его с помощью WebKit и выводить его в виде PDF с высоким качеством.

Для начала установите wkhtmltopdf на свой компьютер. Вы можете загрузить бинарный файл или установить его с Brew на Mac или из своего предпочтительного репозитория Linux.

Вам также нужно установить pdfkit gem , а затем запустить следующий бит Ruby, чтобы сгенерировать PDF с текстом «Hello Envato!»

1
2
3
4
5
6
7
require «pdfkit»
 
kit = PDFKit.new(<<-HTML)
  <p>Hello Envato!</p>
HTML
 
kit.to_file(«hello.pdf»)

У вас должен быть новый файл с именем hello.pdf с текстом вверху.

Пример сгенерированного PDF

PDFKit также позволяет создавать PDF из URL. Если вы хотите создать PDF-файл с главной страницы Google, вы можете запустить:

1
2
3
require «pdfkit»
 
PDFKit.new(‘https://www.google.com’, :page_size => ‘A3’).to_file(‘google.pdf’)

Как видите, я указываю page_size — по умолчанию используется A4. Вы можете увидеть полный список вариантов здесь .

Пример домашней страницы Google в формате PDF

Ранее я упоминал, что мы собираемся генерировать файлы PDF с использованием HTML и CSS. В этом примере я добавил немного CSS для стилизации HTML для образца счета, как вы можете видеть:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
require «pdfkit»
 
kit = PDFKit.new(<<-HTML)
  <style>
    * {
      color: grey;
    }
    h1 {
      text-align: center;
      color: black;
      margin-bottom: 100px;
    }
    .notes {
      margin-top: 100px;
    }
 
    table {
      width: 100%;
    }
    th {
      text-align: left;
      color: black;
      padding-bottom: 15px;
    }
  </style>
 
  <h1>Envato Invoice</h1>
 
  <table>
    <thead>
        <tr>
          <th>Description</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
          <tr>
            <td>Monthly Subscription to Tuts+</td>
            <td>$15</td>
          </tr>
      </tbody>
  </table>
 
  <div class=»notes»>
    <p><strong>Notes:</strong> This invoice was paid on the 23rd of March 2016 using your credit card ending on 1234.</p>
  </div>
HTML
 
kit.to_file(«envato_invoice.pdf»)

Если вы запустите этот скрипт, будет сгенерирован файл envato_invoice.pdf . Эта фотография показывает результат образца счета:

Пример счета-фактуры Envato PDF

Как видите, PDFKit очень прост в использовании, если вы уже знакомы с HTML и CSS. Вы можете продолжить настройку или стилизацию этого документа по своему усмотрению.

Теперь давайте посмотрим, как использовать PDFKit в контексте приложения Rails, чтобы мы могли динамически генерировать PDF-файлы, используя данные из наших моделей. В этом разделе мы собираемся создать простое приложение rails для динамической генерации предыдущего «счета-фактуры Envato». Начните с создания нового приложения rails и добавления трех моделей:

1
2
3
4
5
6
7
$ rails new envato_invoices
$ cd envato_invoices
 
$ rails generate model invoice date:date client notes
$ rails generate model line_item description price:float invoice:references
 
$ rake db:migrate

Теперь нам нужно добавить несколько примеров данных в базу данных. Добавьте этот фрагмент кода в db/seeds.rb .

1
2
3
4
5
6
7
8
line_items = LineItem.create([
    { description: ‘Tuts+ Subscription April 2016’, price: 15.0 },
    { description: ‘Ruby eBook’, price: 9.90} ])
Invoice.create(
    client: ‘Pedro Alonso’,
    total: 24.90,
    line_items: line_items,
    date: Date.new(2016, 4, 1))

Запустите rake db:seed в вашем терминале, чтобы добавить образец счета в базу данных.

Мы также заинтересованы в создании списка счетов-фактур и детализации одного счета-фактуры в нашем приложении, поэтому, используя rails Generator, запустите rails generate controller Invoices index show для создания контроллера и представлений.

приложение / контроллеры / invoices_controller.rb

1
2
3
4
5
6
7
8
9
class InvoicesController < ApplicationController
  def index
    @invoices = Invoice.all
  end
 
  def show
    @invoice = Invoice.find(params[:id])
  end
end

приложение / просмотров / счета / index.html.erb

1
2
3
4
5
6
7
8
<h1>Invoices</h1>
<ul>
  <% @invoices.each do |invoice|
  <li>
    <%= link_to «#{invoice.id} — #{invoice.client} — #{invoice.date.strftime(«%B %d, %Y»)} «, invoice_path(invoice) %>
  </li>
  <% end %>
</ul>

Нам нужно изменить маршруты rails, чтобы по умолчанию перенаправлять их на InvoicesController , поэтому отредактируйте config/routes.rb :

1
2
3
4
5
Rails.application.routes.draw do
  root to: ‘invoices#index’
 
  resources :invoices, only: [:index, :show]
end

Запустите ваш rails server и перейдите к localhost: 3000, чтобы увидеть список счетов:

Список счетов

приложение / просмотров / счета / show.html.erb

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div class=»invoice»>
  <h1>Envato Invoice</h1>
 
  <h3>To: <%= @invoice.client %></h3>
  <h3>Date: <%= @invoice.date.strftime(«%B %d, %Y») %></h3>
 
  <table>
    <thead>
        <tr>
          <th>Description</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        <% @invoice.line_items.each do |line_item|
          <tr>
            <td><%= line_item.description %></td>
            <td><%= number_to_currency(line_item.price) %></td>
          </tr>
        <% end %>
        <tr class=»total»>
          <td style=»text-align: right»>Total: </td>
          <td><%= number_to_currency(@invoice.total) %>
        </tr>
      </tbody>
  </table>
 
  <% if @invoice.notes %>
  <div class=»notes»>
    <p><strong>Notes:</strong> <%= @invoice.notes %></p>
  </div>
  <% end %>
</div>

CSS для этой страницы сведений о счете был перемещен в app / assets / stylesheets / application.scss

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
.invoice {
  width: 700px;
  max-width: 700px;
  border: 1px solid grey;
  margin: 50px;
  padding: 50px;
 
  h1 {
    text-align: center;
    margin-bottom: 100px;
  }
  .notes {
    margin-top: 100px;
  }
 
  table {
    width: 90%;
    text-align: left;
  }
  th {
    padding-bottom: 15px;
  }
 
  .total td {
    font-size: 20px;
    font-weight: bold;
    padding-top: 25px;
  }
}

Затем, когда вы щелкнете по счету на главной странице листинга, вы увидите детали:

Просмотр счета

На данный момент мы готовы добавить функциональность в наше приложение rails для просмотра или загрузки счетов в формате PDF.

Чтобы отобразить счета из нашего приложения rails в PDF, нам нужно добавить в Gemfile три гема: PDFKit , render_anywhere и wkhtmltopdf-binary. По умолчанию рельсы позволяют отображать шаблоны только из контроллера, но с помощью render_anywhere мы можем отображать шаблон из модели или фонового задания.

1
2
3
gem ‘pdfkit’
gem ‘render_anywhere’
gem ‘wkhtmltopdf-binary’

Чтобы не загрязнять наши контроллеры слишком большой логикой, я собираюсь создать новый класс InvoicePdf внутри папки app/models чтобы обернуть логику для создания PDF.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
require «render_anywhere»
 
class InvoicePdf
  include RenderAnywhere
 
  def initialize(invoice)
    @invoice = invoice
  end
 
  def to_pdf
    kit = PDFKit.new(as_html, page_size: ‘A4’)
    kit.to_file(«#{Rails.root}/public/invoice.pdf»)
  end
 
  def filename
    «Invoice #{invoice.id}.pdf»
  end
 
  private
 
    attr_reader :invoice
 
    def as_html
      render template: «invoices/pdf», layout: «invoice_pdf», locals: { invoice: invoice }
    end
end

Этот класс просто принимает счет для отображения в качестве параметра конструктора класса. as_html метод as_html является   чтение шаблона представления layout_pdf invoices/pdf и layout_pdf который мы используем для генерации HTML-кода, который нам нужно представить в формате PDF. Наконец, метод to_pdf   использует PDFKit для сохранения файла PDF в общей папке rails.

Возможно, вы хотите создать динамическое имя в вашем реальном приложении, чтобы PDF-файл не был перезаписан случайно. Возможно, вы захотите сохранить файл в AWS S3 или в личной папке, но это выходит за рамки данного руководства.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div class=»invoice»>
  <h1>Envato Invoice</h1>
 
  <h3>To: <%= invoice.client %></h3>
  <h3>Date: <%= invoice.date.strftime(«%B %d, %Y») %></h3>
 
  <table>
    <thead>
        <tr>
          <th>Description</th>
          <th>Price</th>
        </tr>
      </thead>
      <tbody>
        <% invoice.line_items.each do |line_item|
          <tr>
            <td><%= line_item.description %></td>
            <td><%= number_to_currency(line_item.price) %></td>
          </tr>
        <% end %>
        <tr class=»total»>
          <td style=»text-align: right»>Total: </td>
          <td><%= number_to_currency(invoice.total) %>
        </tr>
      </tbody>
  </table>
 
  <% if invoice.notes %>
  <div class=»notes»>
    <p><strong>Notes:</strong> <%= invoice.notes %></p>
  </div>
  <% end %>
</div>
01
02
03
04
05
06
07
08
09
10
11
12
<!DOCTYPE html>
<html>
<head>
  <title>Envato Invoices</title>
  <style>
    <%= Rails.application.assets.find_asset(‘application.scss’).to_s %>
  </style>
</head>
<body>
  <%= yield %>
</body>
</html>

В этом файле макета следует обратить внимание на то, что мы визуализируем стили в макете. WkHtmlToPdf работает лучше, если мы визуализируем стили таким образом.

На этом этапе нам нужен маршрут и контроллер, который вызывает класс InvoicePdf для отправки PDF-файла в браузер, поэтому отредактируйте config/routes.rb чтобы добавить вложенный ресурс:

1
2
3
4
5
6
7
Rails.application.routes.draw do
  root to: «invoices#index»
 
  resources :invoices, only: [:index, :show] do
    resource :download, only: [:show]
  end
end

Если мы запускаем rake routes , мы видим список маршрутов, доступных в приложении:

1
2
3
4
5
Prefix Verb URI Pattern Controller#Action
            root GET / invoices#index
invoice_download GET /invoices/:invoice_id/download(.:format) downloads#show
        invoices GET /invoices(.:format) invoices#index
         invoice GET /invoices/:id(.:format) invoices#show

Добавьте app/controllers/downloads_controller.rb :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
class DownloadsController < ApplicationController
 
  def show
    respond_to do |format|
      format.pdf { send_invoice_pdf }
    end
  end
 
  private
 
  def invoice_pdf
    invoice = Invoice.find(params[:invoice_id])
    InvoicePdf.new(invoice)
  end
 
  def send_invoice_pdf
    send_file invoice_pdf.to_pdf,
      filename: invoice_pdf.filename,
      type: «application/pdf»,
      disposition: «inline»
  end
end

Как вы можете видеть, когда запрос запрашивает файл PDF, метод send_invoice_pdf обрабатывает запрос. Метод invoice_pdf просто находит счет-фактуру из базы данных по идентификатору и создает экземпляр InvoicePdf. Затем send_invoice_pdf просто вызывает метод to_pdf , чтобы отправить сгенерированный файл PDF в браузер.

Стоит отметить, что мы передаем расположение параметра disposition: "inline" в send_file . Этот параметр отправляет файл в браузер, и он будет отображен. Если вы хотите принудительно загрузить файл, тогда вам нужно будет передать disposition: "attachment" .

Добавьте кнопку загрузки в ваш шаблон шоу- app/views/invoices/show.html.erb :

1
2
3
4
<%= link_to «Download PDF»,
   invoice_download_path(@invoice, format: «pdf»),
   target: «_blank»,
   class: «download» %>

Запустите приложение, перейдите к деталям счета-фактуры, нажмите «Загрузить», и откроется новая вкладка, отображающая счет-фактуру в формате PDF.

Просмотр счета PDF динамически генерируется

Когда вы работаете над разметкой для своего PDF, необходимость генерировать PDF каждый раз, когда вы хотите проверить изменение, иногда может быть медленной. По этой причине возможность просмотра HTML-кода, который будет преобразован в формат PDF, может быть очень полезной. Нам нужно только отредактировать /app/controllers/downloads_controller.rb .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DownloadsController < ApplicationController
 
  def show
    respond_to do |format|
      format.pdf { send_invoice_pdf }
 
      if Rails.env.development?
        format.html { render_sample_html }
      end
    end
  end
 
  private
 
  def invoice
    Invoice.find(params[:invoice_id])
  end
 
  def invoice_pdf
    InvoicePdf.new(invoice)
  end
 
  def send_invoice_pdf
    send_file invoice_pdf.to_pdf,
      filename: invoice_pdf.filename,
      type: «application/pdf»,
      disposition: «inline»
  end
 
  def render_sample_html
    render template: «invoices/pdf», layout: «invoice_pdf», locals: { invoice: invoice }
  end
end

Теперь метод show также отвечает на запросы HTML в режиме разработки. Маршрут для счета в формате PDF будет выглядеть примерно так : http: // localhost: 3000 / invoices / 1 / download.pdf . Если вы измените его на http: // localhost: 3000 / invoices / 1 / download.html , вы увидите счет в HTML с использованием разметки, которая используется для создания PDF.

Учитывая приведенный выше код, генерация PDF-файлов с использованием Ruby on Rails проста, если вы знакомы с языком Ruby и инфраструктурой Rails. Возможно, самым приятным аспектом всего процесса является то, что вам не нужно изучать какие-либо новые языки разметки или особенности создания PDF.

Я надеюсь, что этот урок оказался полезным. Пожалуйста, оставляйте любые вопросы, комментарии и отзывы в комментариях, и я буду рад продолжить.