RubyでLaTeXソースから画像を生成する方法

Ruby用MathMLライブラリというのがある。けっこうすごいんだけど、数学関係のLaTeXソースに限って使うものだし、当然全てのLaTeXの記法がこれで使えるわけじゃない。そういう意味では、HTMLに載せるうえでLaTeXの式を画像に変換するという従来からある方法も、まだまだ見限れるわけじゃないってことだ。

そこで、以下のサイトを参考にして LaTeXソースから画像を生成するスクリプト Ruby版 を作ってみた。

使い方は簡単で、

ruby latex2png.rb texファイル [解像度 [縮小率]]

とすればよい。やってみるとわかるがデフォルトの 576 以下の解像度を指定すると、画像が部分的に欠けたり少し変になる。576以上の 72 (画面解像度)の倍数を指定するとうまくいくように思える。

TESTしてみる

TEST1

参考サイトにある次のLaTeXソースコードを、math.tex として保存

\documentclass[a4j]{jarticle}
  \pagestyle{empty}
  \begin{document}
  \[S_{n} = a_{1}^{2} + a_{2}^{2} + a_{3}^{2} + ... + a_{n}^{2}\]
  \end{document}

これをlatex2pngにくわす

$ ruby latex2png.rb ruby_test/math.tex

するとtest/以下に、math.dvi などのファイルとともに math.small.png ができている。ブラウザでみるとこんなかんじ。

Mathsmall

TEST2

もうひとつやってみよう。例えば数学の文書で使うけど、MathMLライブラリではちょっとムリかも...というやつの好例に数直線の図がある。サンプルとして次のLaTeXソースを math2.tex として保存。

\documentclass[a4j,12pt]{jbook}
\pagestyle{empty}
\begin{document}
       
\[
\begin{picture}(500,60)
\thicklines
\put(5,30){\vector(1,0){410}}
\thinlines
\put(20,28){\line(0,1){5}}
\put(30,28){\line(0,1){5}}
\put(40,28){\line(0,1){5}}
\put(50,28){\line(0,1){5}}
\put(40,36){$-3$}
\put(60,28){\line(0,1){5}}
\put(70,28){\line(0,1){5}}
\put(62,17){$\frac{-13}{5}$}
\put(80,28){\line(0,1){5}}
\put(90,28){\line(0,1){5}}
\put(100,28){\line(0,1){5}}
\put(90,36){$-2$}
\put(110,28){\line(0,1){5}}
\put(120,28){\line(0,1){5}}
\put(130,28){\line(0,1){5}}
\put(122,17){$\frac{-7}{5}$}
\put(140,28){\line(0,1){5}}
\put(150,28){\line(0,1){5}}
\put(140,36){$-1$}
\put(160,28){\line(0,1){5}}
\put(170,28){\line(0,1){5}}
\put(180,28){\line(0,1){5}}
\put(190,28){\line(0,1){5}}
\put(182,17){$\frac{-1}{5}$}
\put(200,28){\line(0,1){5}}
\put(196,36){$0$}
\put(196,17){$\frac{0}{5}$}
\put(210,28){\line(0,1){5}}
\put(207,17){$\frac{1}{5}$}
\put(220,28){\line(0,1){5}}
\put(217,17){$\frac{2}{5}$}
\put(230,28){\line(0,1){5}}
\put(240,28){\line(0,1){5}}
\put(250,28){\line(0,1){5}}
\put(247,36){$1$}
\put(247,17){$\frac{5}{5}$}
\put(260,28){\line(0,1){5}}
\put(270,28){\line(0,1){5}}
\put(267,17){$\frac{7}{5}$}
\put(280,28){\line(0,1){5}}
\put(290,28){\line(0,1){5}}
\put(300,28){\line(0,1){5}}
\put(297,36){$2$}
\put(295,17){$\frac{10}{5}$}
\put(310,28){\line(0,1){5}}
\put(320,28){\line(0,1){5}}
\put(330,28){\line(0,1){5}}
\put(325,17){$\frac{13}{5}$}
\put(340,28){\line(0,1){5}}
\put(350,28){\line(0,1){5}}
\put(347,36){$3$}
\put(360,28){\line(0,1){5}}
\put(370,28){\line(0,1){5}}
\put(380,28){\line(0,1){5}}
       
\put(415,17){$l$}
\end{picture}
\]
       
\end{document}

たかだか数直線でこれだけ書かなきゃいけないんだからウンザリだが、一度書けるとひながたとしても使いたくなる。この math2.tex を画像に変換するコマンドと結果の画像。

$ ruby latex2png.rb ruby_test/math2.tex 720 15

今度は解像度とサイズを指定してみた。ブラウザでみるとこんなかんじ(クリックで拡大表示)。

Math2small_2

TEST3

LaTeX文書に埋め込まれた画像はちゃんと変換されるのか?今度は、Tgif2TeX で書き出したグラフを読み込んだtexファイルを読ませてみる。 Tgif で 書き出した .obj ファイルを、Tgif2TeX で読み込む。

tgif2tex root2graph.obj

root2graph.tps, root2graph.dps, root2graph.ps の 3つのファイルが生成される。これらを画像を作りたいDIRに移動し、このうちの root2graph.tps を読み込んだ math3.tex を書く。

\documentclass[a4j]{jarticle}
\usepackage{graphicx} % required for `\includegraphics' (yatex added)
\pagestyle{empty}
       
\begin{document}
       
\scalebox{0.5}{
  \input{./root2graph.tps}
}
       
\end{document}

math3.tex から math3.small.png を作る。

$ ruby latex2png.rb ruby_test/math3.tex
This is pTeX, Version 3.14159-p3.1.5 (euc) (Web2C 7.4.5)
(./math3.tex
pLaTeX2e <2005/01/04>+0 (based on LaTeX2e <2001/06/01> patch level 0)
(/usr/share/texmf/ptex/platex/base/jarticle.cls
Document Class: jarticle 2002/04/09 v1.4 Standard pLaTeX class
.....................(略)..............................
Output written on math3.dvi (1 page, 812 bytes).
Transcript written on math3.log.
platex math3.tex
.....................(略)..............................
dvips -E -D 576 math3.dvi
gs -q -dNOPAUSE -sDEVICE=png256 -sOUTPUTFILE=math3.png -r576 -g3456x3216 -- math3.trim.ps
convert -scale 33% -contrast math3.png math3.small.png
Complete!

「Complete!」がでれば、math3.small.png ができている。ブラウザでみるとこんなかんじ(クリックで拡大表示)。

Math3small_3


Ruby版 latex2png

ソースは以下のようになっている。参考にしたサイトのものは PHPスクリプトで、.dvi や .psファイルがすでに書き出されている場合は、上書きしない設定になっているが、大きさ調整などで何度も試したいときなどにちょっと不便なので、Ruby版は、CommandExecutor#do_command の中の FileTest.exist? による分岐はコメントアウトしてある。

##### latex2png.rb #####
### input_file is a LaTeX source file converted into PNG image.
### usage: ruby latex2png.rb input_file [resolution [scale]]
       
# default
resolution = 576
scale = 33                      # [%]
       
usage = "Usage: ruby latex2png.rb input_file [resolution [scale]]\n\tinput_file is a LaTeX source file (xxx.tex) converted into PNG image."
@working_dir = nil
       
class CommandExecutor
  def initialize(dir=".")
    @exec_dir = dir
  end
       
  def do_command( command, testfile )
    result = nil
#    if (!FileTest.exist?("#{@exec_dir}/#{testfile}"))
      Dir.chdir(@exec_dir) {
        result = system( command )
        unless result
          puts "Error: command failed (#{command})"
          exit
        end
      }
#    end
    # show command execution message
    puts command.to_s
    return result
  end
end
       
class TrimData
  attr_reader :width, :height
       
  def initialize(dir=".")
    @exec_dir = dir
    @ps = @trim = @res = nil
    @width = @height = 0
  end
       
  def read_bounding_box( file_path )
    begin
      psfile = File.open( file_path )
      psfile.flock(File::LOCK_SH)
    rescue => ex
      puts ex.message
    end
       
    bb = Array.new
       
    psfile.each { |line|
      if line =~ /%%BoundingBox:/
        bb = line.scan( /%%BoundingBox: *([0-9]+) +([0-9]+) +([0-9]+) +([0-9]+)/ ).flatten!
        break
      end
    }
       
    if bb.empty?
      puts "BoundingBox size not found in ps file (#{@ps})."
      exit
    end
       
    return bb
  end
       
  def mk_trimps(ps, trim, res)
    @ps = ps
    @trim = trim
    @res = res
       
    psfile_path = "#{@exec_dir}/#{@ps}"
    bb = read_bounding_box( psfile_path )
       
    x1, y1, x2, y2 = bb.collect {|x| x.to_i }
    @width = (x2 - x1) * ( @res / 72 )
    @height = (y2 - y1) * ( @res / 72 )
       
    trim_cont = <<"EOS"
/NumbDict countdictstack def
1 dict begin
/showpage {} def
userdict begin
-#{bb[0]}.000000 -#{bb[1]}.000000 translate
1.000000 1.000000 scale
0.000000 0.000000 translate
(#{@ps}) run
countdictstack NumbDict sub {end} repeat
showpage
       
EOS
       
    begin
      trimfp = File.open("#{@exec_dir}/#{@trim}", 'w')
      trimfp.flock(File::LOCK_EX)
    rescue => ex
      puts ex.message
    end
       
    trimfp.puts( trim_cont )
    trimfp.close
  end
end
       
class DataAttr
  attr_reader :latex, :dvi, :ps, :trim, :png, :small, :resolution, :scale
  def set( name, res, scale )
    @latex = name + '.tex';
    @dvi   = name + '.dvi';
    @ps    = name + '.ps';
    @trim  = name + '.trim.ps';
    @png   = name + '.png';
    @small = name + '.small.png';
    @resolution = res.to_i
    @scale = scale.to_i
  end
end
       
if ( ARGV[0].nil? )
  puts usage
  exit
end
       
latexfile = ARGV[0]
latex_file_path = File.expand_path(ARGV[0])
@working_dir = File.dirname(latex_file_path)
executor = CommandExecutor.new(@working_dir)
data = DataAttr.new
trim = TrimData.new(@working_dir)
       
if (!FileTest.exist?(latex_file_path) || !FileTest.readable?(latex_file_path))
  puts "File: latexfile does not exist or cat not read."
  puts usage
  exit
else
  name = File.basename(latexfile, ".tex")
       
  if (ARGV[1])
    resolution = ARGV[1].to_i
    if (ARGV[2])
      scale = ARGV[2].to_i
    end
  end
       
  data.set(name, resolution, scale)
end
       
       
# make dvi
command = "platex #{data.latex}"
executor.do_command( command, data.dvi )
       
# make ps
command = "dvips -E -D #{data.resolution} #{data.dvi}"
executor.do_command( command, data.ps )
       
# make trim.ps
trim.mk_trimps( data.ps, data.trim, data.resolution )
       
# get trimming data
width = trim.width
height = trim.height
       
# make png
command = "gs -q -dNOPAUSE -sDEVICE=png256 -sOUTPUTFILE=#{data.png} -r#{data.resolution} -g#{width}x#{height} -- #{data.trim}"
executor.do_command( command, data.png )
       
# make small.png
command = "convert -scale #{data.scale}% -contrast #{data.png} #{data.small}"
executor.do_command( command, data.small )
       
if (FileTest.exist?("#{@working_dir}/#{data.small}"))
  puts "Complete!"
end
       
       
__END__

aBowman

別荘はこちら

  • Marbles2
    音楽、美術、映画、本など趣味的なページはここに移転しました。考えるのが面倒だったので、タイトルは単に2です。
2016年11月
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30      

mail

  • 82pkdick@gmail.com

最近のトラックバック

無料ブログはココログ