« Ruby on xfy : 拡張コマンドをRubyで作る! | メイン | 作ってみませんか? Blog Editor 拡張コマンドの「革新」に期待! »

Rubyの世界にはワクワクがいっぱい! あれ?

2008年8月20日 (水)

なんか一月前は「Javaの世界には…」と言っていた気がしますが、今は目の前の Rubyに集中、集中! 今回は、Rubyの追加モジュールを使って Blog Editor の拡張コマンドを作ってみましょう。キラッ☆

その前に、「ScriptEngine プラグイン for ExtCommander」 についての大事なお知らせです。ExtCommander のフレームワークを便利に使ってもらえるように、いくつかのオブジェクトをあらかじめ登録してあります。

_owner

Windowオブジェクト。これを親に指定してダイアログを開いてください。

_selectedstring

エントリー編集画面で選択されている文字列。

_xmlstring

エントリーデータのXML文字列。

Rubyの場合は $_owner などとして参照できます。ダイアログを開く際などに必要になりますので、覚えておいてください。_selectedstring を使うと、ウィキペディアリンク のような拡張コマンドが作成できます。_xmlstring はXMLですので、使用する場合はパーズする必要があります。ブラウザープレビュープラグイン はこのデータを使用しています。

さて、Ruby (や他のスクリプト言語)が人気がある理由の1つは『豊富な外部拡張モジュールを容易に追加できる』ということではないかと思っています。今回は、GEM で追加できる exifrrmagick4j というモジュールを使ったサンプルをご紹介します。

exifr を使ったサンプル。

EXIF情報付きで写真をブログに貼り付けられます。

rmagick4j を使ったサンプル。

ポラロイド写真風に画像を加工できます。

では、サンプルスクリプトをご紹介します!

exifr と rmagick4j は、事前にGEMでインストールしておいてくださいね。詳細は略!
(gem install exifr とか gem install rmagick4j とかやるだけ)

次に、サンプルスクリプトです。2つあります。ちょぉっと長いですが、「隣のRuby-ist」氏の力作ですので、是非実行してみてください。

まず、デジカメの写真からEXIF情報を取得して、写真と一緒にブログに貼り付けるサンプルです。

exif_sample.rb

$KCODE = "UTF8"
require 'rubygems'
require 'exifr'
require 'date'
require 'java'

include_class('java.io.File') {|package,name| "J#{name}"}
include_class 'javax.swing.JDialog'
include_class 'javax.swing.JPanel'
include_class 'javax.swing.JLabel'
include_class 'javax.swing.JButton'
include_class 'javax.swing.JTextField'
include_class 'javax.swing.JFileChooser'

#
#=HTMLの取得. 引数 _image_path_, _width_, _height_ には画像へのファイルパス、表示したい幅と高さを指定します。
#
def get_html(image_path, width, height = "auto")
  width      = width  + "px"
  height     = height + "px" unless height == "auto"
  image_html = <<EOL
<a href="file:/#{image_path}">
<img src="file:/#{image_path}" alt="" style="width:#{width};height:#{height};" />
</a>
EOL
  html = ""

  begin
    exif_info  = EXIFR::JPEG.new(image_path)
  rescue
    html = "<div style=\"text-align:center;\">#{image_html}</div>"
    return html
  end

  if exif_info.exif? then
    date_time = DateTime.parse(exif_info.date_time_original.to_s)
    html = <<EOL
<div style="margin:1em;text-align:center;">#{image_html}<br />
<table style="margin-left:auto;margin-right:auto;">
<caption>#{File.basename(image_path, ".*")}</caption>
<tbody>
  <tr><th>カメラ</th><td>#{exif_info.make} #{exif_info.model}</td></tr>
  <tr><th>撮影日時</th><td>#{date_time.strftime("%Y/%m/%d %H:%M")}</td></tr>
  <tr><th>シャッター速度</th><td>#{exif_info.exposure_time.to_s}</td></tr>
  <tr><th>露出</th><td>#{exif_info.f_number.to_f}</td></tr>
  <tr><th>焦点距離(35mm換算)</th><td>#{exif_info.focal_length_in_35mm_film}mm</td></tr>
  <tr><th>ISO</th><td>#{exif_info.iso_speed_ratings}</td></tr>
</tbody>
</table>
</div>
EOL
  else
    html = "<div style=\"text-align:center;\">#{image_html}</div>"
  end

  return html
end

#
#=JPEG画像ファイルフィルタ
#
class JpegFilter < javax.swing.filechooser.FileFilter

  def accept(file)
    return true if file.isDirectory
    filename = file.getName
    extname  = File.extname(filename).downcase
    if extname == ".jpg" || extname == ".jpeg"
      return true
    else
      return false
    end
  end

  def getDescription
    return "JPEG images(*.jpg, *.jpeg)"
  end

end

#
#=JPEG画像のExif情報を表示する
#
dialog = JDialog.new($_owner, "Exif Sample", true)
panel  = JPanel.new(java.awt.FlowLayout.new)
button = JButton.new("Image Select")
width_label = JLabel.new("WIDTH")
width_field = JTextField.new("320", 4)

# ボタンクリック時の動作
button.addActionListener do |event|
  chooser = JFileChooser.new
  chooser.addChoosableFileFilter(JpegFilter.new)
  chooser.setMultiSelectionEnabled(true)
  chooser.setFileSelectionMode(JFileChooser::FILES_ONLY)
  selected = chooser.showOpenDialog(dialog)
  if selected == JFileChooser::APPROVE_OPTION then
    files = chooser.getSelectedFiles
    files.each do |file|
      puts get_html(file.getAbsolutePath, width_field.getText)
    end
    dialog.dispose
  end
end

# 画像表示幅の値補正
width_field.addFocusListener do |event|
  event.source.setText(event.source.getText.to_i.to_s)
  event.source.setText("320") unless event.source.getText.to_i > 0
end

panel.add(width_label)
panel.add(width_field)
panel.add(button)
dialog.add(panel)
dialog.setDefaultCloseOperation(JDialog::DISPOSE_ON_CLOSE)
dialog.setSize(320, 80)
dialog.setLocationRelativeTo(nil)
dialog.setVisible(true)

こんなダイアログが出たら、「Image Select」ボタンを押してデジカメ画像を選びます。

すると、次のような形式のHTMLを生成するので、それをエントリーに貼り付けることができます。

次に、ポラロイド風の画像を作成するサンプルです。

rmagick_sample.rb

$KCODE="UTF8"
require 'rubygems'
gem PLATFORM == 'java' ? 'rmagick4j' : 'rmagick'
require 'RMagick'
require 'tempfile'
require 'java'

include_class 'javax.swing.JDialog'
include_class 'javax.swing.JPanel'
include_class 'javax.swing.JLabel'
include_class 'javax.swing.JButton'
include_class 'javax.swing.JTextField'
include_class 'javax.swing.JFileChooser'
include_class 'javax.swing.JColorChooser'
include_class 'java.awt.dnd.DnDConstants'
include_class 'java.awt.datatransfer.DataFlavor'
include_class 'javax.swing.ImageIcon'

#
#=インスタント写真風画像作成
#
class ImagePoraroid
  def initialize(filename = "")
    set_image(filename) unless filename == ""
  end

  def set_image(filename)
    @image = Magick::Image.read(filename).first
  end

  #
  #インスタント写真風画像作成
  #
  def polaroid(width, title = "", font_size = 24, background_color = "#ffffff")
    image = @image

    # 画像リサイズ
    image = image.resize_to_fit(width) unless width == image.columns

    # 画像フォーマットをPNGに変換
    image.format = "PNG" unless image.format == "PNG"

    # ボーダー作成
    image = self.border(image)

    # テキスト作成
    image = self.annotate(image, title, font_size) unless title == ""

    # ドロップシャドウ作成
    image = self.shadow(image, background_color)

    # 回転
    image.background_color = "red"
    image.rotate!(-5) do
      self.background_color = "red"
    end

    return image
  end

  #
  #=インスタント写真風画像作成
  #
  def polaroid!(width, title = "", font_size = 24, background_color = "#ffffff")
    @image = self.polaroid(width, title, font_size, background_color)
  end

  #
  # ボーダー作成
  #
  def border(image)
    border_width  = 20
    border_height = 60
    background = Magick::Image.new(image.columns + border_width,
                                   image.rows + border_height) do
      self.background_color = "#fcfcfa"
    end
    border_x_offset = 10
    border_y_offset = 10
    image = background.composite(image,
                                 border_x_offset,
                                 border_y_offset,
                                 Magick::OverCompositeOp)
    return image
  end

  #
  # テキスト作成
  #
  def annotate(image, str, font_size)
      draw = Magick::Draw.new
      margin_bottom = 10
      draw.annotate(image, 0, 0, 0, margin_bottom, str) do
        self.pointsize = font_size.to_i
        self.gravity = Magick::SouthGravity
        #  self.font_family = "Arial"
        self.fill = 'black'
      end
    return image
  end

  #
  # ドロップシャドウ作成
  #
  def shadow(image, background_color = "#ffffff")
    shadow_x_offset = 10
    shadow_y_offset = 10
    shadow_border_width  = 5
    shadow_border_height = 5
    background_width  = image.columns + shadow_x_offset + shadow_border_width
    background_height = image.rows    + shadow_x_offset + shadow_border_height

    background = Magick::Image.new(background_width, background_height) do
      self.background_color = background_color
    end
    shadow = Magick::Image.new(image.columns, image.rows) do
      self.background_color = "gray75"
    end
    background = background.composite(shadow,
                                      shadow_x_offset,
                                      shadow_y_offset,
                                      Magick::OverCompositeOp)
    background = background.blur_image(0, 4)

    image = background.composite(image,
                                 0,
                                 0,
                                 Magick::OverCompositeOp)
    return image
  end

  #
  # 画像ファイル保存
  #
  def image_write(filename, image = @image)
    image.write(filename)
  end

end

#
#=ダイアログボックス作成
#
class RDialog
  include java.awt.dnd.DropTargetListener

  def initialize
    @target_image_path      = ""
    @destination_image_path = ""

    @dialog          = JDialog.new($_owner, "Beyond ImageEasy", true)
    @panel           = JPanel.new(java.awt.FlowLayout.new)
    @image_button    = JButton.new("Image")
    @width_label     = JLabel.new("Image Width")
    @width_field     = JTextField.new("200", 3)
    @font_size_label = JLabel.new("Font Size")
    @font_size_field = JTextField.new("14", 2)
    @caption_label   = JLabel.new("Caption")
    @caption_field   = JTextField.new("", 20)
    @color_label     = JLabel.new("Background Color")
    @color_field     = JTextField.new("#ffffff", 7)
    @preview_button  = JButton.new("Preview")
    @preview_button.setEnabled(false)
    @ok_button       = JButton.new("OK")
    @ok_button.setEnabled(false)
    @image_label     = JLabel.new(ImageIcon.new)
    @preview_panel   = JPanel.new(java.awt.FlowLayout.new)
    @preview_panel.setBackground(java.awt.Color::WHITE)

    # 各ボタンクリック時の動作
    @preview_button.addActionListener do |event|
      preview(@target_image_path) unless @target_image_path == ""
    end
    @ok_button.addActionListener do |event|
      ok_btn
    end
    @image_button.addActionListener do |event|
      # ファイル選択
      chooser = JFileChooser.new
      chooser.addChoosableFileFilter(ImageFilter.new)
      chooser.setFileSelectionMode(JFileChooser::FILES_ONLY)
      selected = chooser.showOpenDialog(@dialog)
      if selected == JFileChooser::APPROVE_OPTION then
        file = chooser.getSelectedFile
        @target_image_path = file.getAbsolutePath
        preview(@target_image_path)
        @preview_button.setEnabled(true) if @preview_button.isEnabled == false
      end
    end

    # 画像幅の値補正
    @width_field.addFocusListener do |event|
      event.source.setText(event.source.getText.to_i.to_s)
      event.source.setText("200") unless event.source.getText.to_i > 0
    end
    # フォントサイズの値補正
    @font_size_field.addFocusListener do |event|
      event.source.setText(event.source.getText.to_i.to_s)
      event.source.setText("14") unless event.source.getText.to_i > 0
    end
    # 背景カラーの値補正
    @color_field.addFocusListener do |event|
      event.source.setText(event.source.getText.strip)
      event.source.setText("#ffffff") unless event.source.getText =~ /^#[\da-f]{6}/
    end

    # ドラッグ&ドロップ処理登録
    target = java.awt.dnd.DropTarget.new
    target.addDropTargetListener(self)
    @dialog.setDropTarget(target)

    @panel.add(@width_label)
    @panel.add(@width_field)
    @panel.add(@font_size_label)
    @panel.add(@font_size_field)
    @panel.add(@caption_label)
    @panel.add(@caption_field)
    @panel.add(@color_label)
    @panel.add(@color_field)
    @panel.add(@image_button)
    @panel.add(@preview_button)
    @panel.add(@ok_button)
    @preview_panel.add(@image_label)
    @panel.add(@preview_panel)
    @dialog.add(@panel)
    @dialog.setDefaultCloseOperation(JDialog::DISPOSE_ON_CLOSE)
    @dialog.setSize(500, 400)
    @dialog.setLocationRelativeTo(nil)
    @dialog.setVisible(true)
  end

  #
  # 画像ファイルかどうか拡張子で判別
  #
  def image_file?(file_path)
    extensions = [".png", ".jpg", ".jpeg", ".gif"]
    extname    = File.extname(file_path).downcase
    if extensions.include?(extname) then
      return true
    else
      return false
    end
  end

  #
  # プレビュー
  #
  def preview(file_path)
    unless file_path == ""
      image_width = @width_field.getText.to_i
      caption     = @caption_field.getText
      font_size   = @font_size_field.getText.to_i
      color       = @color_field.getText.strip
      ip = ImagePoraroid.new(file_path)

      # ポラロイド風画像処理
      ip.polaroid!(image_width, caption, font_size, color)

      # 画像ファイル保存
      tmp_filename = "image" + Time.now.to_i.to_s + ".png"
      @destination_image_path = File.join(Dir.tmpdir, tmp_filename)
      ip.image_write(@destination_image_path)

      # 画像表示
      image_icon  = ImageIcon.new(@destination_image_path)
      @image_label.setIcon(image_icon)
      @ok_button.setEnabled(true) if @ok_button.isEnabled == false
      @preview_panel.setBackground(java.awt.Color.decode(color))
    end
  end

  def ok_btn
    # XHTML出力
    print "<img src=\"file:/#{@destination_image_path}\" alt=\"\" />"
    @dialog.dispose
  end

  def dragEnter(event)
    transfer = event.getTransferable
    if transfer.isDataFlavorSupported(DataFlavor::javaFileListFlavor) then
      files = transfer.getTransferData(DataFlavor::javaFileListFlavor)
      files.each do |file|
        event.rejectDrag unless image_file?(file.getPath)
      end
    else
       event.rejectDrag
    end
  end

  def dragExit(event)
  end

  def dragOver(event)
  end

  def dropActionChanged(event)
  end

  def drop(event)
    transfer = event.getTransferable

    if transfer.isDataFlavorSupported(DataFlavor::javaFileListFlavor) then
      event.acceptDrop(DnDConstants::ACTION_COPY_OR_MOVE)
      files = transfer.getTransferData(DataFlavor::javaFileListFlavor)
      # 複数のファイルがドラッグされていても最初のファイルのみ処理
      @target_image_path = files.first.absolute_path if image_file?(files.first.path)
      preview(@target_image_path)
      @preview_button.setEnabled(true) if @preview_button.isEnabled == false
    end

  end

  #
  #=画像ファイルフィルタ
  #
  class ImageFilter < javax.swing.filechooser.FileFilter

    def accept(file)
      return true if file.isDirectory
      filename = file.getName
      extname  = File.extname(filename).downcase
      extensions = [".jpg", ".jpeg", ".png", ".gif"]

      if extensions.include?(extname) then
        return true
      else
        return false
      end
    end

    def getDescription
      return "images(*.jpg, *.jpeg, *.png, *.gif)"
    end

  end

end

RDialog.new

こんなダイアログが表示されたら、「Image」ボタンを押して画像ファイルを選択してください。「Caption」に文字列を入力して「Preview」ボタンを押せば文字列も書き加えられます。

「OK」ボタンを押すと次のような画像が作成されて、エントリーに貼り付けることができます。

これらのスクリプトは、コピー&ペーストしてScriptEngine プラグインのスクリプト領域に貼り付けて実行することもできるし、スクリプトをファイルに保存して「スクリプトからファイルを読み込み」メニューで読み込んで実行してもOKです。「貼り付け」ボタンを押すと実行した結果をエントリーに貼り付けることができます。

スクリプトなので、自分好みに改変することも簡単にできます。rmagick_sample.rb での画像回転角度を変えたりフォントを変えたり、自由にいじってみてください。

いやぁ、スクリプトって便利ですね。

しか~し!

拡張コマンドとして考えると、コマンドを実行するためにいちいちスクリプトを貼り付けるのは面倒ですよね。次回はもっと簡単に拡張コマンドとして使える方法をご紹介します。「メイクアップ:スクリプト編」です。

コメント

コメントを投稿

コメントは記事の投稿者が承認するまで表示されません。