|
||||
|
Глава 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<library shelf="Recent Acquisitions"> <section name="Ruby"> <book isbn="0672328844"> <title>The Ruby Way</title> <author>Hal Fulton</author> <description>Second edition. The book you are now reading. Ain't recursion grand? </description> </book> </section> <section name="Space"> <book isbn="0684835509"> <title>The Case for Mars</title> <author>Robert Zubrin</author> <description>Pushing toward a second home for the human race. </description> </book> <book isbn="074325631X"> <title>First Man: The Life of Neil A. Armstrong</title> <author>James R. Hansen</author> <description>Definitive biography of the first man on the moon. </description> </book> </section> </library> 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 Вот что он напечатает: <book isbn='0672328844'> ... </> 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 программ. |
|
||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||
|