Программирование на языке Ruby.

15.4. Создание документов в формате PDF с помощью библиотеки PDF::Writer.

Библиотека PDF::Writer предназначена для создания PDF-документов из программы на языке Ruby. Ее можно установить из gem-пакета или скачать с сайта RubyForge. Последовательность создания документа проста:

Require 'rubygems'.

Require 'pdf/writer'.

Pdf = PDF::Writer.new.

15.4.1. Основные концепции и приемы.

Одна из серьезных проблем, встающих перед любым дизайнером документов, - текстовые шрифты. Библиотека PDF::Writer поддерживает пять основных шрифтов, причем первые три допускают полужирное и курсивное начертание:

• Times-Roman.

• Helvetica.

• Courier.

• ZapfDingbats.

• Symbol.

Если шрифт не указан, по умолчанию предполагается Helvetica. При выборе шрифта можно создать таблицу замены символов, которая позволяет имитировать символы, не имеющие графического начертания или отсутствующие в кодовой странице. В шрифтах Times-Roman, Helvetica и Courier по 315 печатаемых символов (из них у 149 есть предопределенные байтовые коды); в шрифте Symbol — 190 символов (у 189 есть предопределенные коды), а в шрифте ZapfDingbats — 202 символа (всем соответствуют коды). Шрифты представлены в кодировке Adobe, но в момент выбора шрифта отдельные символы можно переопределить.

Текущая версия не позволяет напечатать все 315 символов, определенных в шрифтовом файле, поскольку после того как шрифт выбран, изменить таблицу замены символов уже невозможно. В последующих версиях PDF::Writer эта проблема будет решена.

В следующем примере мы задали для PDF-документа шрифт Times-Roman. Программа чтения PDF-файлов будет считать, что текст представлен в кодировке WinAnsiEncoding, но вместо символа с кодом 0x01 подставит глиф «lozenge» (ромб), еще увидим его ниже (листинг 15.11).

Pdf.select_font "Times-Roman",

 { :encoding => "WinAnsiEncoding",

  :differences => {0x01 => "lozenge"}.

 }

Библиотека PDF::Writer располагает средствами для форматирования текста и создания таблиц, которые хорошо документированы. Не так очевидно, что пока не срабатывает автоматическая разбивка на страницы, можно форматировать страницу вручную весьма любопытными способами. С помощью переноса осей и масштабирования мы можем нарисовать четыре страницы на одной.

В текущей версии PDF::Writer (1.1.3) каждая такая «страница» должна полностью умещаться на одной физической странице. Если в дело вмешивается механизм автоматического разбиения на страницы, то будет создана новая физическая страница. В следующих версиях усовершенствованный вариант этой техники будет работать и для многоколонных страниц.

Для демонстрации создадим метод quadrant (листинг 15.10). Он войдет также составной частью в длинный пример из следующего раздела, который преследует две цели: показать, как создается документ из четырех страниц и как можно разместить четыре страницы PDF-документа на одной странице книги, сэкономив тем самым место.

Листинг 15.10. Метод quadrant.

Def quadrant(pdf, quad).

 raise unless block_given?

 mx = pdf.absolute_x_middle.

 my = pdf.absolute_y_middle.

 pdf.save_state.

 case quad.

  when :ul.

   pdf.translate_axis(0, my).

  when :ur.

   pdf.translate_axis(mx, my).

  when :ll.

   nil # pdf.translate_axis(0, 0).

  when :lr.

   pdf.translate_axis(mx, 0).

 end.

 pdf.scale_axis(0.5, 0.5).

 pdf.у = pdf.page_height.

 yield.

 pdf.restore_state.

End.

Здесь каждая страница целиком строится в отдельном блоке. Таким образом, мы можем изменять масштаб и положение осей, никак не затрагивая код построения страницы. Первым делом мы, конечно, сохраняем текущее состояние. Это позволит нам не восстанавливать вручную масштаб и начало системы координат по завершении работы. Перед тем как приступать к конструированию, мы помещаем начало координат квадранта в нужное место страницы (pdf.translate_axis x, y).

Предположим, что начало координат находится не в точке (0, 0), а в точке (50, 50). Тогда отрезок из точки (15, 20) в точку (35, 40) на самом деле будет соединять точки с координатами (65, 70) и (85, 90). Но код рисования отрезка об этом ничего не знает.

После переноса оси (то есть сдвига начала координат) мы можем изменить масштаб вдоль оси. Чтобы получить четыре квадранта, следует уменьшить вдвое масштаб по осям X и Y ( pdf.scale_axis 0.5, 0.5). Иными словами, если бы сейчас я провел отрезок между точками (0, 0) и (90, 90), то без переноса осей он соединял бы точки с физическими координатами (0, 0) и (45, 45), а с переносом — точки с координатами (90, 90) и (135, 135). В любом случае будет проведена линия вдоль диагонали длиной 90 единиц измерения. Просто из-за масштабирования сами единицы стали в два раза меньше.

Затем мы отдаем управление блоку, а когда он закончит работу, восстанавливаем состояние, вызывая предоставленный библиотекой метод restore_state. Иначе пришлось бы вручную увеличивать масштаб вдвое и переносить ось в обратном направлении.

15.4.2. Пример документа.

Для демонстрации рассмотренной выше техники мы создадим четыре страницы в четырех разных квадрантах. Три из них — слегка измененные варианты демонстрационных программ, включённых в дистрибутив PDF::Writer:

• demo.rb, квадрант 1.

• individual-i.rb, квадрант 3.

• gettysburg.rb, квадрант 4.

Четвертая страница (в квадранте 2) не имеет прямого аналога среди демонстрационных программ, она ближе всего к программе chunkybacon.rb.

Весь код приведен в листинге 15.11, а результат показан на рис. 15.4. Пример довольно длинный, мы подробно обсудим его ниже.

Листинг 15.11. Создание демонстрационного документа.

Require 'rubygems'.

Require 'pdf/writer'.

Def quadrant(pdf, quad).

 raise unless block_given?

 mx = pdf.absolute_x_middle.

 my = pdf.absolute_y_middle.

 pdf.save_state.

 case quad.

  when :ul.

   pdf.translate_axis 0, my.

  when :ur.

   pdf.translate_axis mx, my.

  when :ll.

   nil # no translation needed.

  when :lr.

   pdf.translate_axis mx, 0.

 end.

 pdf.scale_axis(0.5, 0.5).

 pdf.у = pdf.page_height.

 yield.

 pdf.restore_state.

End.

Pdf = PDF::Writer.new.

Pdf.select_font("Times-Roman",

 rencoding => "WinAnsiEncoding",

 differences => { 0x01 => "lozenge" }).

Mx = pdf.absolute_x_middle.

My = pdf.absolute_y_middle.

Pdf.line(0, my, pdf.page_width, my).stroke.

Pdf.line(mx, 0, mx, pdf.page_height).stroke.

# Левый верхний: Demo (UL).

Quadrant(pdf, :ul) do.

 x = pdf.absolute_right_margin.

 r1 = 25.

 40.step(1, -3) do |xw|

  tone = 1.0 - (xw / 40.0) * 0.2.

  pdf.stroke_style(PDF::Writer::StrokeStyle.new(xw)).

  pdf.stroke_color(Color::RGB.from_fraction(1, tone, tone)).

  pdf.line(x, pdf.bottom_margin, x,

   pdf.absolute_top_margin).stroke.

  x -= xw+2.

 end.

 40.step(1, -3) do |xw|

  tone = 1.0 - (xw / 40.0) * 0.2.

  pdf.stroke_style(PDF::Writer::StrokeStyle.new(xw)).

  pdf.stroke_color(Color::RGB.from_fraction(1, tone, tone)).

  pdf.circle_at(pdf.left_margin + 10, pdf.margin_height - 15,

   r1).stroke.

  r1 += xw.

 end.

 pdf.stroke_color(Color::RGB::Black).

 x = pdf.absolute_left_margin.

 y = pdf.absolute_bottom_margin.

 w = pdf.margin_width.

 h = pdf.margin_height.

 pdf.rectangle(x, y, w, h).stroke.

 text = "The Ruby Way".

 y = pdf.absolute_top_margin.

 50.step(5, -5) do |size|

  height = pdf.font_height(size).

  y -= height.

  pdf.add_text(pdf.left_margin + 10, y, text, size).

 end.

 (0...360).step(20) do |angle|

  pdf.fill_color(Color::RGB.from_fraction(rand, rand, rand)).

  pdf.add_text(300 + Math.cos(PDF::Math.deg2rad(angle)) * 40,

   300 + Math.sin(PDF::Math.deg2rad(angle)) * 40,

   text, 20, angle).

 end.

End.

Pdf.fill_color Color::RGB::Black.

# Правый верхний: Grampian Highlands (UR).

Quadrant(pdf, :ur) do.

 pdf.image("grampian-highlands.jpg",

  :height => pdf.margin_height,

  :resize => :width).

 pdf.text("The Grampian Highlands, Scotland",

  justification => :center,

  :font_size => 36).

 pdf.text("\001August 2001\001", :justification => :center,

  :font_size => 24).

 pdf.move_pointer(24).

 info = <<-'EOS'.split($/).join(" ").squeeze(" ").

This picture was taken during a driving vacation through the.

Scottish highlands in August 2001 by Austin Ziegler.

 EOS.

 pdf.text(info, :justification => :full, :font_size => 16,

  :left => 100, :right => 100).

End.

Pdf.fill_color Color::RGB::Black.

# Левый нижний: Individual-I (LL).

Quadrant(pdf, :ll) do.

 require 'color/palette/monocontrast'.

 class IndividualI.

  def initialize(size = 100).

   @size = size.

  end.

  # Размер буквы "i" в пунктах.

  attr_accessor :size.

  def half_i(pdf).

   pdf.move_to(0, 82).

   pdf.line_to(0, 78).

   pdf.line_to(9, 78).

   pdf.line_to(9, 28).

   pdf.line_to(0, 28).

   pdf.line_to(0, 23).

   pdf.line_to(18, 23).

   pdf.line_to(18, 82).

   pdf.fill.

  end.

  private :half_i.

  def draw(pdf, x, y).

   pdf.save_state.

   pdf.translate_axis(x, y).

   pdf.scale_axis(1 * (@size / 100.0), -1 * (@size / 100.0)).

   pdf.circle_at(20, 10, 7.5).

   pdf.fill.

   half_i(pdf).

   pdf.translate_axis(40, 0).

   pdf.scale_axis(-1, 1).

   half_i(pdf).

   pdf.restore_state.

  end.

 end.

 ii = IndividualI.new(24).

 x = pdf.absolute_left_margin.

 y = pdf.absolute_top_margin.

 bg = Color::RGB.from_fraction(rand, rand, rand).

 fg = Color::RGB.from_fraction(rand, rand, rand).

 pal = Color::Palette::MonoContrast.new(bg, fg).

 sz = 24.

 (-5..5).each do |col|

  pdf.fill_color pal.background[col].

  ii.draw(pdf, x, y).

  ii.size += sz.

  x += sz / 2.0.

  y -= sz / 2.0.

  pdf.fill_color.

  pal.foreground[col].

  ii.draw(pdf, x, y).

  x += sz / 2.0.

  y -= sz / 2.0.

  ii.size += sz.

 end.

End.

Pdf.fill_color Color::RGB::Black.

# Правый нижний: Gettysburg Address (LR).

# Это текст Геттисбергского обращения Авраама Линкольна.

Quadrant(pdf, :lr) do.

 pdf.text("The Gettysburg Address\n\n",

  :font_size => 36, justification => :center).

 y0 = pdf.y + 18.

 speech = <<-'EOS'.split($/). join(" ").squeeze(" ").

Four score and seven years ago our fathers brought forth on.

This continent a new nation, conceived in liberty and.

Dedicated to the proposition that all men are created equal.

Now we are engaged in a great civil war, testing whether.

That nation or any nation so conceived and so dedicated can.

Long endure. We are met on a great battlefield of that war.

We have come to dedicate a portion of that field as a final.

Resting-place for those who here gave their lives that that.

Nation might live. It is altogether fitting and proper that.

We should do this. But in a larger sense, we cannot.

Dedicate, we cannot consecrate, we cannot hallow.

This ground. The brave men, living and dead who struggled here.

Have consecrated it far above our poor power to add or.

Detract. The world will little note nor long remember what.

We say here, but it can never forget what they did here. It.

Is for us the living rather to be dedicated here to the.

Unfinished work which they who fought here have thus far so.

Nobly advanced. It is rather for us to be here dedicated to.

The great task remaining before us that from these honored.

Dead we take increased devotion to that cause for which they.

Gave the last full measure of devotion that we here highly.

Resolve that these dead shall not have died in vain, that.

This nation under God shall have a new birth of freedom, and.

That government of the people, by the people, for the people.

Shall not perish from the earth.

EOS.

 pdf.text(speech, justification => :full, :font_size => 14,

  :left => 50, :right => 50).

 pdf.move_pointer(36).

 pdf.text("U.S. President Abraham Lincoln, 19 November 1863",

  :justification => :right, :right => 100).

 pdf.text("Gettysburg, Pennsylvania", :justification => :right,

  :right => 100).

 pdf.rounded_rectangle(pdf.left_margin + 25, y0, pdf.margin_width - 50,

  y0 - pdf.y + 18, 10).stroke.

End.

Pdf.save_as("4page.pdf").

Программирование на языке Ruby.

Рис. 15.4. Пример документа, состоящего из четырех страниц в разных квадрантах.

Итак, в четырех квадрантах расположены следующие страницы:

• левый верхний: demo.rb;

• правый верхний: фотография Грампианских холмов, Шотландия;

• левый нижний: individual-i.rb;

• правый нижний: Геттисбергское обращение.

Для краткости будем называть эти квадранты UL, UR, LL и LR. В тексте программы используются соответствующие символы ( :ul и т.д.).

Первый квадрант (UL) заполнен вертикальными линиями, толщина которых постепенно уменьшается, начиная с 40 единиц, с одновременным осветлением. Затем рисуются круги увеличивающегося радиуса, при этом толщина линий уменьшается, а цвет становится светлее. И наконец, выводятся два набора текстов: один — сверху вниз с постепенным уменьшением размера шрифта, а другой — с поворотом вокруг центральной оси как раз там, где кончаются вертикальные линии.

Страница во втором квадранте (UR) содержит картинку и ее описание. Особый интерес представляет строка с датой. Мы вставляем в поток байт с кодом 0x01; при отображении вместо него будет поставлен символ ромба в соответствии с таблицей замены, заданной при выборе шрифта.

В третьем квадранте (UR) с помощью программы Individual-I мы снова демонстрируем технику переноса осей и масштабирования. Самое интересное здесь — инверсия осей. Если по оси выбирается отрицательный масштаб, то команды вывода текста и рисования меняют направление. Следовательно, при рисовании буквы I достаточно задать лишь правила формирования половины рисунка, а потом инвертировать ось X, вызвав метод pdf.scale_axis(-1, 1), и повторить ту же последовательность операций.

Последний квадрант (LR) заполняется сравнительно легко. Мы форматируем и заключаем в прямоугольник со скругленными углами текст речи, которую президент Линкольн произнес в Геттисберге.

Сохранение PDF-документа — воплощенная простота. Если нужно записать его на диск, мы вызываем метод save_as объекта PDF:

Pdf.save_as("4page.pdf").

Нетрудно также отправить PDF-документ браузеру из CGI-программы:

Require 'cgi'.

Cgi = CGI.new.

Out = pdf.render.

Puts <<-EOS.

Content-Type: application/pdf.

Content-Disposition: inline; filename="4page.pdf".

Size: #{out.size}.

EOS.

Конечно, в этом разделе мы сумели затронуть лишь малую толику библиотеки PDF::Writer. Дополнительную информацию ищите в онлайновой документации. Если вы знакомы с форматом PDF, имейте в виду, что библиотека еще развивается и пока не поддерживает спецификацию в полном объеме.