• 15.1. Разбор XML и REXML
  • 15.1.1. Древовидное представление
  • 15.1.2. Потоковый разбор
  • 15.1.3. XPath и другие интерфейсы
  • 15.2. RSS и Atom
  • 15.2.1. Стандартная библиотека rss
  • 15.2.2. Библиотека feedtools
  • 15.3. Обработка изображений при помощи RMagick
  • 15.3.1. Типичные графические задачи
  • 15.3.2. Специальные эффекты и трансформации
  • 15.3.3. API рисования
  • 15.4. Создание документов в формате PDF с помощью библиотеки PDF::Writer
  • 15.4.1. Основные концепции и приемы
  • 15.4.2. Пример документа
  • 15.5. Заключение
  • Глава 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 | Добавить материал | Нашёл ошибку | Наверх