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

Фултон Хэл

Глава 15. Ruby и форматы данных

 

 

В этой главе мы расскажем обо всем понемножку. Значительную часть материала можно было бы поместить и в другое место. Не все рассмотренные вопросы одинаковы по важности и сложности, но каждому из них стоит уделить внимание.

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

Любой из вас припомнит сотни примеров форматов файлов. Это и графические форматы типа JPG, GIF и PNG, и форматы документов (RTF и PDF), и «универсальные» форматы (CSV, XML или YAML) и бесчисленные форматы, разработанные отдельными компаниями, многие из которые являются просто вариациями на тему хранения данных в виде таблицы с фиксированной шириной колонок, столь популярного в древние времена (я имею в виду 1960-е годы).

Один из самых простых и наиболее употребительных форматов данных — обычный текст. Но даже на такой формат можно наложить ту или иную структуру (отсюда и популярность XML). Бывают также чисто двоичные и двоично-текстовые форматы. В принципе можно было бы разработать «иерархию» форматов, подобную сетевой модели ISO, в которой информация представляется по-разному на разных уровнях протоколов.

Но в каком бы формате данные ни хранились, рано или поздно их придется читать, разбирать и снова сохранять. В этой главе мы рассмотрим лишь несколько самых распространенных форматов файлов; в одной книге невозможно охватить все существующие. Если вы хотите разбирать файлы в таких форматах, как vCard, iCal и пр., то придется поискать соответствующие библиотеки или, быть может, написать свою собственную.

 

15.1. Разбор XML и REXML

 

Язык XML (который внешне «похож» на HTML или SGML) стал популярен в 1990-х годах. Благодаря некоторым свойствам он действительно лучше таблицы с фиксированной шириной колонки. Например, он позволяет задавать имена полей, представлять иерархически организованные данные и, самое главное, хранить данные переменной длины.

Конечно, сорок лет назад XML был бы невозможен из-за ограничений на объем памяти. Но представим себе, что он появился бы тогда. Знаменитая проблема 2000 года, которой пресса уделяла так много внимания в 1999 году (хотя проблема-то и яйца выеденного не стоила!) при наличии XML вообще не возникла бы. Ведь причина была в том, что в унаследованных системах данные хранились в формате с фиксированной длиной. Так что, несмотря на некоторые недостатки, у XML есть сферы применения. В Ruby для работы с XML чаще всего применяется библиотека REXML, написанная Шоном Расселом (Sean Russell). Начиная с 2002 года REXML (произносится «рекс-эм-эль») входит в стандартный дистрибутив Ruby.

Сразу отмечу, что REXML работает довольно медленно. Достаточно ли ее быстродействия для вашего конкретного приложения, решать вам. Не исключено, что со временем вам придется перейти на библиотеку libxml2 (которую мы здесь не рассматриваем). Она, конечно, работает очень быстро (поскольку написана на С), но, пожалуй, не так близка по духу к Ruby.

REXML — это процессор XML, написанный целиком на Ruby в полном соответствии со стандартом XML 1.0. Он не проверяет достоверность документа (соответствие схеме) и удовлетворяет всем тестам OASIS (Organization for the Advancement of Structured Information Standards - организация по внедрению стандартов структурирования информации) для таких процессоров.

Библиотека REXML предлагает несколько API. Сделано это, конечно, для того, чтобы обеспечить большую гибкость, а не внести путаницу. Два классических API — интерфейсы на базе DOM (объектной модели документа) и SAX (потоковый интерфейс). В первом случае весь документ считывается в память и хранится в древовидной форме. Во втором разбор осуществляется по мере чтения документа. Этот способ не требует загрузки документа в память и потому применяется, когда документ слишком велик, а память ограничена.

Во всех примерах мы будем использовать один и тот же XML-файл (см. листинг 15.1), представляющий часть описания личной библиотеки.

Листинг 15.1. Файл books.xml

 

 

   The Ruby Way

   Hal Fulton

   Second edition. The book you are now reading.

    Ain't recursion grand?

 

 

 

 

   The Case for Mars

   Robert Zubrin

   Pushing toward a second home for the human

    race.

 

 

   First Man: The Life of Neil A. Armstrong

   James R. Hansen

   Definitive biography of the first man on

    the moon.

 

 

 

15.1.1. Древовидное представление

Сначала покажем, как работать с ХМL-документом, представленным в виде дерева. Для начала затребуем библиотеку rexml/document; обычно для удобства мы включаем также директиву include rexml, чтобы импортировать все необходимое в пространство имен верхнего уровня. В листинге 15.2 продемонстрировано несколько полезных приемов.

Листинг 15.2. Разбор документа с применением DOM

require 'rexml/document'

include REXML

input = File.new("books.xml")

doc = Document.new(input)

root = doc.root

puts root.attributes["shelf"] # Недавние приобретения

doc.elements.each("library/section") { |e| puts e.attributes["name"] }

# Выводится:

#  Ruby

#  Space

doc.elements.each("*/section/book") { |e| puts e.attributes["isbn"] }

# Выводится:

#  0672328844

#  0321445619

#  0684835509

#  074325631X

sec2 = root.elements[2]

author = sec2.elements[1].elements["author"].text # Robert Zubrin

Обратите внимание: атрибуты представляются в виде хэша. Обращаться к элементам можно либо по пути, либо по номеру. В последнем случае учтите, что согласно спецификации XML индексация элементов начинается с 1, а не с 0, как в Ruby.

 

15.1.2. Потоковый разбор

А теперь попробуем разобрать тот же самый файл в потоковом стиле (на практике это вряд ли понадобилось бы, потому что размер файла невелик). У этого подхода несколько вариантов, в листинге 15.3 показан один из них. Идея в том, чтобы определить класс слушателя, методы которого анализатор будет вызывать для обработки событий.

Листинг 15.3. SAX-разбор

require 'rexml/document'

require 'rexml/streamlistener'

include REXML

class MyListener

 include REXML::StreamListener

 def tag_start(*args)

  puts "tag_start: #{args.map {|x| x.inspect}.join(', ')}"

 end

 def text(data)

  return if data =~ /^\w*$/ # Ничего, кроме пропусков.

  abbrev = data[0..40] + (data.length > 40 ? "..." : "")

  puts "  text   :  #{abbrev.inspect}"

 end

end

list = MyListener.new

source = File.new "books.xml"

Document.parse_stream(source, list)

В этом нам поможет класс StreamListener; сам по себе он содержит только заглушки, то есть пустые методы обратного вызова. Вы должны переопределить их в своем подклассе. Когда анализатор встречает открывающий тег, он вызывает метод tag_open. Можете считать это чем-то вроде метода method_missing, которому в качестве параметра передается имя тега (и все его атрибуты в форме хэша). Аналогично работает метод text; о других методах вы можете прочитать в документации на сайте http://ruby-doc.org или в каком-нибудь другом месте.

Программа в листинге 15.3 протоколирует обнаружение каждого открывающего и каждого закрывающего тега. Результат работы показан в листинге 15.4 (для краткости текст приведен не полностью).

Листинг 15.4. Результат работы программы потокового разбора

tag_start: "library", {"shelf"=>"Recent Acquisitions"}

tag_start: "section", {"name"=>"Ruby"}

tag_start: "book", {"isbn"=>"0672328844"}

tag_start: "title", {}

  text   :  "The Ruby Way"

tag_start: "author", {}

  text   :  "Hal Fulton"

tag_start: "description", {}

  text   :  "Second edition. The book you are now read..."

tag_start: "section", {"name"=>"Space"}

tag_start: "book", {"isbn"=>"0684835509"}

tag_start: "title", {}

  text   :  "The Case for Mars"

tag_start: "author", {}

  text   :  "Robert Zubrin"

tag_start: "description", {}

  text   :  "Pushing toward a second home for the huma..."

tag_start: "book", {"isbn"=>"074325631X"}

tag_start: "title", {}

  text   :  "First Man: The Life of Neil A. Armstrong"

tag_start: "author", {}

  text   : "James R. Hansen"

tag_start: "description", {}

  text   : "Definitive biography of the first man on ..."

 

15.1.3. XPath и другие интерфейсы

Альтернативным способом работы с ХМL-документом является язык XPath, с помощью которого описывается, как обратиться к конкретным элементам и атрибутам XML-документа.

Библиотека REXML поддерживает XPath с помощью класса XPath. Предполагается, что документ представлен в виде DOM (см. выше листинг 15.2). Рассмотрим следующий код:

# (Этап подготовки опущен.)

book1 = XPath.first(doc, "//book") # Найдена информация о первой книге

р book1

# Распечатать названия всех книг.

XPath.each(doc, "//title") { |e| puts e.text }

# Получить массив всех элементов "author".

names = XPath.match(doc, "//author").map {|x| x.text }

p names

Вот что он напечатает:

...

The Ruby Way

The Case for Mars

First Man: The Life of Neil A. Armstrong

["Hal Fulton", "Robert Zubrin", "James R. Hansen"]

REXML поддерживает также API на основе стандарта SAX2 (с некоторыми добавлениями в духе Ruby) и экспериментальный анализатор на основе технологии «вытягивания». Они в этой книге не рассматриваются - можете обратиться к сайту http://ruby-doc.org или аналогичному ресурсу.

 

15.2. RSS и Atom

 

Часто изменяющийся контент распространяется в Интернете с помощью синдицированных каналов, или просто каналов. Обычно данные описываются на некотором диалекте языка XML.

Наверное, из всех форматов подобного рода наиболее распространен формат RSS. Эта аббревиатура означает Rich Site Summary (обогащенная сводка сайта), хотя некоторые расшифровывают ее как RDF Site Summary, понимая под RDF Resource Description Format (формат описания ресурса).

В сети Web очень много временной или часто изменяемой информации: записи в блогах, статьи в онлайновых журналах и т.д. Канал представляет собой естественный способ распространения и синдицирования такого контента.

Еще одним популярным форматом является Atom; некоторые даже считают, что он превосходит RSS. Но вообще-то сейчас предпочитают говорить не «RSS-канал» или «Atom-канал», а просто «канал».

Мы вкратце рассмотрим обработку форматов RSS и Atom. В первом случае применяется стандартная библиотека Ruby, во втором — библиотека, еще не вошедшая в стандартный дистрибутив.

 

15.2.1. Стандартная библиотека rss

Формат RSS основан на XML, поэтому разбирать его можно как обычный XML-документ. Но, поскольку это все-таки специализированный вариант, для него имеет смысл разработать специальный анализатор. Кроме того, запутанность стандарта RSS уже стала притчей во языцех — некорректно написанные программы могут генерировать такие RSS-документы, которые будет очень трудно разобрать.

Ситуация осложняется еще и тем, что существуют несовместимые версии стандарта; чаще всего используются 0.9,1.0 и 2.0. В общем, подобно производству колбасы, RSS — такая вещь, в детали которой лучше не вникать.

В дистрибутив Ruby входит стандартная библиотека, понимающая версии стандарта 0.9,1.0 и 2.0. Даже если вы не укажете версию входного документа явно, библиотека попытается определить ее самостоятельно.

Рассмотрим пример. Мы загрузили канал с сайта http://marsdrive.com и распечатали заголовки нескольких статей из него:

require 'rss'

require 'open-uri'

URL = "http://www.marstoday.com/rss/mars.xml"

open(URL) do |h|

 resp = h.read

 result = RSS::Parser.parse(resp,false)

 puts "Канал: #{result.channel.title}"

 result.iterns.each_with_index do |item,i|

  i += 1

  puts "#{i} #{item.title}"

 end

end

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

В этой программе мы для удобства воспользовались библиотекой open-uri. Подробно мы рассмотрим ее в главе 18, а пока достаточно знать, что она позволяет вызывать метод open для URI, как для обычного файла.

Отметим, что канал извлекает из документа анализатор RSS, а наша программа печатает название канала. Кроме того, метод доступа items формирует список элементов канала, то есть статей, а мы распечатываем их заголовки.

Понятно, что результат меняется со временем; когда я запускал эту программу, она напечатала вот что:

Title: Mars Today Top Stories

1 NASA Mars Picture of the Day: Lava Levees

2 NASA Mars Global Surveyor TES Dust And Temperature Maps 25 June - 2 July 2006

3 Mars Institute Core Team Arrives at the HMP Research Station on Devon Island

4 Assessment of NASA's Mars Architecture 2007-2016

5 NASA Mars Picture of the Day: Rush Hour

Есть также возможность генерировать документы в формате RSS (листинг 15.5). Для этого нужно инвертировать показанную выше процедуру.

Листинг 15.5. Создание RSS-канала

require 'rss'

feed = RSS::Rss.new("2.0")

chan = RSS::Rss::Channel.new

chan.description = "Feed Your Head"

chan.link = "http://nosuchplace.org/home/"

img = RSS::Rss::Channel::Image.new

img.url = "http://nosuchplace.org/images/headshot.jpg"

img.title = "Y.T."

img.link = chan.link

chan.image = img

feed.channel = chan

i1 = RSS::Rss::Channel::Item.new

i1.title = "Once again, here we are"

i1.link = "http://nosuchplace.org/articles/once_again/"

i1.description = "Don't you feel more like you do now than usual?"

i2 = RSS::Rss::Channel::Item.new

i2.title = "So long, and thanks for all the fiche"

i2.link = "http://nosuchplace.org/articles/so_long_and_thanks/"

i2.description = "I really miss the days of microfilm..."

i3 = RSS::Rss::Channel::Item.new

i3.title = "One hand clapping"

i3.link = "http://nosuchplace.org/articles/one_hand_clapping/"

i3.description = "Yesterday I went to an amputee convention..."

feed.channel.items << i1 << i2 << i3

puts feed

Большая часть этой программы понятна без слов. Мы создаем канал в формате RSS 2.0 (с пустыми элементами channel и image), а потом с помощью методов доступа добавляем данные. Элемент image ассоциируется с элементом channel, а последний — с самим RSS-каналом.

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

feed.channel. items = [i1,i2,i3]

но такое решение работать не будет. Почему-то в классе Channel нет акцессора items=. Можно было бы написать items[0] = i1 и т.д., или то же самое в цикле. Наверное, есть и другие способы добиться нужного результата, но представленное выше решение вполне годится.

У библиотеки rss есть еще много возможностей, но не все в данный момент хорошо документированы. Если вы не сможете найти то, что вам нужно, в качестве последнего средства обратитесь к исходным текстам.

Многие предпочитают не RSS, a Atom. Библиотека rss не умеет работать с этим форматом, но есть прекрасная (хотя и не стандартная) библиотека feedtools. Мы рассмотрим ее в следующем разделе.

 

15.2.2. Библиотека feedtools

Библиотека feedtools (распространяемая в виде gem-пакета) — плод работы Боба Амана (Bob Aman). Она более или менее единообразно работает с обоими форматами RSS и Atom и сохраняет все данные в общем внутреннем формате (основанном преимущественно на Atom). В нее встроены собственные средства для работы с IRI, так что явно включать библиотеки net/http или open-uri не требуется.

Вот простой пример, эквивалентный первому примеру из предыдущего раздела:

require 'feed_tools'

URL = "http://www.marstoday.com/rss/mars.xml"

feed = FeedTools::Feed.open(URL)

puts "Description: #{feed.title}\n"

feed.entries.each_with_index {|x,i| puts "#{i+1} #{x.title}" }

Этот вариант короче и яснее предыдущего. Некоторые вещи не так очевидны, например у объекта feed нет явного метода channel. Однако такие методы, как title и description можно вызывать непосредственно для объекта feed, поскольку канал может быть только один.

Ниже показано, как читать новости из канала в формате Atom:

require 'feedtools'

URL = "http://www.atomenabled.org/atom.xml"

feed = FeedTools::Feed.open(URL)

puts "Description: #{feed.title}\n"

feed.entries.each_with_index {|x,i| puts "#{i+1} #{x.title}" }

Обратите внимание — изменился только сам URL! Это замечательно, поскольку мы можем обрабатывать каналы независимо от формата. Результат, естественно, похож на то, что мы видели раньше:

Description: AtomEnabled.org

1 AtomEnabled's Atom Feed

2 Introduction to Atom

3 Moving from Atom 0.3 to 1.0

4 Atom 1.0 is Almost Final

5 Socialtext Supports Atom

Хочу еще раз предостеречь вас: не тратьте впустую ресурсы сервера, принадлежащего поставщику канала. Реальное приложение должно кэшировать содержимое канала, а если вы занимаетесь просто тестированием, лучше создайте собственный канал. Библиотека feedtools поддерживает довольно развитый механизм кэширования в базе данных, которого должно хватить для большинства применений.

А теперь добавим к предыдущему примеру еще две строки:

str = feed.build_xml("rss",2.0)

puts str

Мы только что преобразовали канал Atom в канал RSS 2.0. А можно было бы вместо этого указать RSS 0.9 или RSS 1.0. Возможно и преобразование в обратном направлении: прочитать новости из RSS-канала и записать их в Atom-канал. Это одна из сильных сторон библиотеки.

Во время работы над книгой текущей версией библиотеки feedtools была 0.2.25. Вероятно, со временем изменится и набор возможностей, и API.

 

15.3. Обработка изображений при помощи RMagick

 

Последние пятнадцать лет на нас обрушивается все больше и больше графической информации. В качестве основного поставщика «услады для глаз» во всех формах компьютеры уже обогнали телевизоры. А значит, программистам приходится манипулировать графическими данными, представленными в различных форматах. На языке Ruby это лучше всего делать с помощью библиотеки RMagick, которую написал Тим Хантер (Tim Hunter).

RMagick — это привязка к Ruby библиотеки ImageMagick (или ее ветви, GraphicsMagick). Устанавливается она как gem-пакет, но для работы нужно еще установить одну из базовых библиотек (IM или GM). Если вы работаете в Linux, то, вероятно, та или другая библиотека уже имеется, а, если нет, можете загрузить ее с сайта http://imagemagick.org (или http://graphicsmagick.org).

Поскольку RMagick — лишь привязка, то спрашивать, какие графические форматы она поддерживает, — все равно что спрашивать, какие форматы поддерживает базовая библиотека. Все наиболее распространенные, в частности JPG, GIF, PNG, TIFF наряду с десятками других.

То же относится и к операциям, поддерживаемым RMagick. Они ограничены лишь возможностями базовой библиотеки, поскольку RMagick дублирует весь ее API. Кстати говоря, API не только функционально богат, но и и является прекрасным примером API «в духе Ruby»: в нем привычно используются символы, блоки и префиксы методов, так что большинству программистов Ruby он покажется интуитивно очевидным.

Заметим попутно, что API очень объемный. Ни этой главы, ни даже всей книги целиком не хватило бы для рассмотрения всех его деталей. В следующих разделах мы дадим лишь общее представление об RMagick, а полную информацию вы можете найти на сайте проекта (http://rmagick.rubyforge.org).

 

15.3.1. Типичные графические задачи

Одна из самых простых и распространенных задач, связанных с графическим файлом, — получение характеристик изображения (ширина и высота в пикселях и т.д.). Посмотрим, как можно извлечь эти метаданные.

Рис. 15.1. Два примера изображений

На рис. 15.1 приведены два простых изображения, на которые мы будем ссылаться в этом и последующих примерах. Первое (smallpic.jpg) — просто абстрактная картинка, созданная в графическом редакторе; в ней присутствуют несколько оттенков серого цвета, а также прямые и кривые линии. Второе — фотография старенького автомобиля, которую я сделал в 2002 году в сельском районе Мексики. Для книги оба изображения переведены в черно-белый формат. В листинге 15.6 показано, как извлечь из соответствующих файлов необходимую информацию.

Листинг 15.6. Получение информации об изображении

гequire 'RMagick'

def show_info(fname)

 img = Magick::Image::read(fname).first

 fmt = img.format

 w,h = img.columns, img.rows

 dep = img.depth

 nc = img.number_colors

 nb = img.filesize

 xr = img.x_resolution

 yr = img.y_resolution

 res = Magick::PixelsPerInchResolution ? "дюйм" : "см"

 puts <<-EOF

 Файл: #{fname}

 Формат: #{fmt}

 Размеры: #{w}x#{h} пикселей

 Цветов: #{nc}

 Длина файла: #{nb} байтов

 Разрешение: #{xr}/#{yr} пикселей на #{res}

 EOF

 puts

end

show_info("smallpic.jpg")

show_info("vw.jpg")

Вот результат работы этой программы:

Файл:smallpic.jpg

Формат: JPEG

Размеры: 257x264 пикселей

Цветов: 248

Длина файла:19116 байтов

разрешение: 72.0/72.0 пикселей на дюйм

Файл: vw.Jpg

Формат: JPEG

размеры: 640x480 пикселей

Цветов: 256

Длина файла:55892 байтов

Разрешение: 72.0/72.0 пикселей на дюйм

2.0 pixels per inch

Посмотрим, как именно работает эта программа. Для чтения файла мы вызываем метод Magick::Image::read. Поскольку один файл (например, анимированный GIF) может содержать несколько изображений, эта операция возвращает массив изображений (мы получаем лишь первое, вызывая метод first). Для чтения файла можно также воспользоваться методом Magick::ImageList.new.

У объекта, представляющего изображение, есть ряд методов чтения: format (название формата изображения), filesize, depth и другие. Не так очевидно, что для получения ширины и высоты изображения служат методы columns и rows соответственно (поскольку изображение представляется в виде прямоугольной таблицы пикселей). Разрешение представляется двумя числами, так как может быть разным по вертикали и горизонтали.

Можно получить и другие метаданные об изображении. Подробнее об этом вы можете прочитать в онлайновой документации по RMagick.

Часто возникает необходимость перевести изображение из одного формата в другой. В RMagick это проще всего сделать, прочитав изображение из файла в одном из поддерживаемых форматов и записав его в другой файл. Новый формат определяется расширением имени файла. Понятно, что «за кулисами» при этом происходит преобразование данных. Пример:

img = Magick::Image.read("smallpic.jpg")

img.write("smallpic.gif") # Преобразовать в формат GIF.

Иногда нужно изменить размер изображения (сделать его больше или меньше). Для этого обычно применяется один из четырех методов: thumbnail, resize, sample и scale. Все они принимают либо число с плавающей точкой (коэффициент масштабирования), либо два числа (новые размеры в пикселях). Различия между этими методами продемонстрированы в листинге 15.7. Если вас волнует быстродействие, рекомендую провести тесты на своем компьютере, используя собственные данные.

Листинг 15.7. Четыре способа масштабирования изображения

require 'RMagick'

img = Magick::ImageList.new("vw.jpg")

# Все эти методы могут принимать либо один параметр - коэффициент

# масштабирования, либо два - ширину и высоту.

# Метод thumbnail самый быстрый, особенно если нужно получить очень

# маленькое изображение.

pic1 = img.thumbnail(0.2)   # Уменьшить до 20%.

pic2 = img.thumbnail(64,48) # Новый размер - 64x48 пикселей.

# resize работает со средней скоростью. Если заданы третий и четвертый

# параметры, то они интерпретируются как фильтр и размывание

# соответственно. По умолчанию подразумевается фильтр LanczosFilter

# и коэффициент размывания 1.0.

pic3 = img.resize(0.40)     # Уменьшить до 40%.

pic4 = img.resize(320,240)  # Новый размер - 320x240.

pic5 = img.resize(300,200,Magick::LanczosFilter,0.92)

# Метод sample также имеет среднее быстродействие (и не выполняет

# интерполяцию цветов).

pic6 = img.sample(0.35)     # Уменьшить до 35%.

pic7 = img.sample(320,240)  # Новый размер - 320x240.

# Метод scale в моих тестах оказался самым медленным.

pic8 = img.scale(0.60)      # Уменьшить до 60%.

pic9 = img.scale(400,300)   # Новый размер - 400x300.

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

 

15.3.2. Специальные эффекты и трансформации

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

В листинге 15.8 показано 12 различных эффектов. Метод example принимает имя файла, символ, соответствующий методу, и имя нового файла; он читает файл, вызывает метод и записывает результат в новый файл. Сами методы (скажем, do_rotate) по большей части просты — они получают изображение и вызывают его метод экземпляра (а возвращают результат трансформации).

Листинг 15.8. Двенадцать специальных эффектов и трансформаций

require 'Rmagick'

def do_flip(img)

 img.flip

end

def do_rotate(img)

 img.rotate(45)

end

def do_implode(img)

 img = img.implode(0.65)

end

def do_resize(img)

 img.resize(120,240)

end

def do_text(img)

 text = Magick::Draw.new

 text.annotate(img, 0, 0, 0, 100, "HELLO") do

  self.gravity = Magick::SouthGravity

  self.pointsize = 72

  self.stroke = 'black'

  self.fill = '#FAFAFA'

  self.font_weight = Magick::BoldWeight

  self.font_stretch = Magick::UltraCondensedStretch

 end

 img

end

def do_emboss(img)

 img.emboss

end

def do_spread(img)

 img.spread(10)

end

def do_motion(img)

 img.motion_blur(0,30,170)

end

def do_oil(img)

 img.oil_paint(10)

end

def do_charcoal(img)

 img.charcoal

end

def do_vignette(img)

 img.vignette

end

def do_affine(img)

 spin_xform = Magick::AffineMatrix.new(1, Math::PI/6, Math::PI/6, 1, 0, 0)

 img.affine_transform(spin_xform) # Применить преобразование.

end

###

def example(old_file, meth, new_file)

 img = Magick::ImageList.new(old_file)

 new_img = send(meth, img)

 new_img.write(new_file)

end

example("smallpic.jpg", :do_flip,    "flipped.jpg")

example("smallpic.jpg", :do_rotate,  "rotated.jpg")

example("smallpic.jpg", :do_resize,  "resized.jpg")

example("smallpic.jpg", :do_implode, "imploded.jpg")

example("smallpic.jpg", :do_text,    "withtext.jpg")

example("smallpic.jpg", :do_emboss,  "embossed.jpg")

example("vw.jpg", :do_spread,   "vw_spread.jpg")

example("vw.jpg", :do_motion,   "vw_motion.jpg")

example("vw.jpg", :do_oil,      "vw_oil.jpg")

example("vw.jpg", :do_charcoal, "vw_char.jpg")

example("vw.jpg", :do_vignette, "vw_vig.jpg")

example("vw.jpg", :do_affine,   "vw_spin.jpg")

Мы продемонстрировали методы flip, rotate, implode, resize, annotate и др. Результаты представлены на рис. 15.2.

Рис. 15.2. Двенадцать специальных эффектов и трансформаций

О том, какие еще существуют трансформации изображений, читайте в онлайновой документации.

 

15.3.3. API рисования

В RMagick имеется развитый API для рисования линий, многоугольников и различных кривых. Он поддерживает заливку, полупрозрачность, выбор цвета, шрифтов, вращение, растяжение и другие операции.

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

В листинге 15.9 приведена программа, которая рисует на заданном фоне сетку, а поверх нее несколько закрашенных геометрических фигур. Черно-белое изображение, получившееся в результате, показано на рис. 15.3.

Листинг 15.9. Простая программа рисования

require 'RMagick'

img = Magick::ImageList.new

img.new_image(500, 500)

purplish = "#ff55ff"

yuck = "#5fff62"

bleah = "#3333ff"

line = Magick::Draw.new

50.step(450,50) do |n|

 line.line(n,50, n,450) # Вертикальная прямая.

 line.draw(img)

 line.line(50,n, 450,n) # Горизонтальная прямая.

 line.draw(img)

end

# Нарисовать круг.

cir = Magick::Draw.new

cir.fill(purplish)

cir.stroke('black').stroke_width(1)

cir.circle(250,200, 250,310)

cir.draw(img)

rect = Magick::Draw.new

rect.stroke('black').stroke_width(1)

rect.fill(yuck)

rect.rectangle(340,380,237,110)

rect.draw(img)

tri = Magick::Draw.new

tri.stroke('black').stroke_width(1)

tri.fill(bleah)

tri.polygon(90,320,160,370,390,120)

tri.draw(img)

img = img.quantize(256,Magick::GRAYColorspace)

img.write("drawing.gif")

Рис. 15.3. Простая программа рисования

Рассмотрим эту программу подробнее. Сначала мы создаем «пустое» изображение методом ImageList.new, а потом вызываем для возвращенного объекта метод new_image. Можно считать, что мы получили «чистый холст» заданного размера (500×500 пикселей).

Для удобства определим несколько цветов с понятными именами, например purplish и yuck. Цвета определяются так же, как в HTML. Базовая библиотека xMagick сама распознает много названий цветов, например, red и black; если сомневаетесь, пробуйте или задавайте цвета в шестнадцатеричном виде.

Затем мы создаем объект рисования line; это объект Ruby, соответствующий графическому объекту, который мы видим на экране. Переменную иногда называют gc или как-то похоже (от «graphics context» — графический контекст), но нам кажется естественным употребить имя, отражающее природу объекта.

Далее вызывается метод line объекта рисования, по два раза на каждой итерации цикла. Взглянув на то, как изменяются координаты, вы поймете, что на каждой итерации рисуется одна горизонтальная и одна вертикальная прямая.

После каждого обращения к line мы вызываем метод draw того же объекта и передаем ему ссылку на изображение. Именно на этом шаге графический объект помещается на холст.

Лично меня обращения вида shape.draw(image) немного путают. В общем случае вызов любого метода выглядит так:

big_thing.operation(little_thing)

# Например: dog.wag(tail) (собака.вилять(хвост))

Но методы RMagick записываются, скорее, в виде:

little_thing.operation(big_thing)

# Продолжая аналогию: tail.wag(dog) (хвост.вилять(собака))

Впрочем, эта идиома достаточно распространена, особенно в программах рисования и графических интерфейсах. И в классической объектно-ориентированной идеологии это вполне оправданно: фигура знает, как нарисовать себя, а стало быть, должна иметь метод draw. Он же, в свою очередь, должен знать, где рисовать, поэтому ему нужно передать ссылку на холст (или что-то подобное).

Но, возможно, вас не мучает вопрос, от имени какого объекта следует вызывать метод. Тем лучше!..

Покончив с сеткой, мы переходим к рисованию фигур. Метод circle принимает в качестве параметров центр окружности и какую-нибудь точку на ней (радиус не передается!). Метод rectangle еще проще; для рисования прямоугольника нужно задать координаты левого верхнего угла (первые два параметра) и координаты правого нижнего угла (последние два параметра). Треугольник же является частным случаем многоугольника; мы задаем координаты всех его вершин, а замыкающий отрезок (из конечной точки в начальную) рисуется автоматически.

У каждого графического объекта есть еще несколько методов. Взгляните на этот «сцепленный» вызов:

shape.stroke('black').stroke_width(1)

Это что-то вроде пера, которое рисует черными чернилами линию толщиной в один пиксель. Цвет штриха часто имеет значение, особенно если мы хотим закрасить фигуру.

Конечно, у каждой из трех этих фигур есть еще метод fill, при вызове которого указывается цвет заливки. (Имеются также более сложные способы заливки, например, штриховкой, с наложением тени и т.д.) Метод fill заменяет цвет внутренних пикселей фигуры указанным, ориентируясь на цвет границы, чтобы отличить внутреннюю часть от внешней.

API рисования содержит также методы для настройки полупрозрачности, пространственных преобразований и многого другого. Есть методы для анализа, рисования и манипулирования текстовыми строками. Существует даже специальный RVG API (Ruby Vector Graphics — векторная графика в Ruby), совместимый с рекомендацией консорциума W3C по масштабируемой векторной графике (SVG).

Мы не можем привести здесь документацию по всем этим бесчисленным возможностям. Дополнительную информацию вы можете найти на сайте http://rmagick.rubyforge.org.

 

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

Рис. 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, имейте в виду, что библиотека еще развивается и пока не поддерживает спецификацию в полном объеме.

 

15.5. Заключение

В этой главе мы показали, как с помощью библиотеки REXML можно разбирать XML-документы, представленные в виде дерева DOM или потока. Познакомились мы и с интерфейсом REXML к языку XPath.

Был продемонстрирован разбор информации из новостных каналов, представленных в формате на базе XML. Библиотека rss умеет работать только с форматом RSS, а библиотека feedtools понимает форматы RSS и Atom (и умеет преобразовывать из одного в другой).

Мы также видели, как можно читать и манипулировать графическими изображениями разного формата с помощью библиотеки RMagick. Рассмотрели мы и API рисования, позволяющий включать в изображение произвольный текст и геометрические фигуры. Наконец, мы показали, как с помощью библиотеки PDF::Writer можно создавать из программы сложные PDF-документы высокого качества.

Следующая глава посвящена совсем другой теме. Речь пойдет об эффективном тестировании и отладке написанных на Ruby программ.