remarkdown を巡る冒険 (原題: remarkdown versus Sphinx)

docutils はある形式の文書を別の形式に変換するフレームワークです。
このフレームワークを利用して rst2html や rst2latex などが提供されています。

しかし、docutils が標準で対応している入力形式は reST 形式に限られているので、
実際には reST 変換用のライブラリとして捉えられていました。

markdown 黄金時代

docutils や Sphinx の周囲では reST を中心としてドキュメントが記述されていますが、
2014年現在、軽量マークアップ言語の主流の座は markdown であると言っても過言ではないでしょう。
flavor が乱立していたり、拡張性に乏しかったり、困ったら HTML で書き下してあったりと、
記述能力が十分とはいえない markdown ですが、github を初めてとして様々な場所で用いられています。
4月1日はすでに過ぎたというのに Internet Draft になっているほどの人気ぶりです。

Sphinx に興味を持った人も、Sphinx と markdown の組み合わせを実現するために様々な試行錯誤を繰り返しています。

remarkdown の到来

そんな流れの中、remarkdown という docutils 拡張が発見されます。
remarkdown は docutils の拡張として動作し、reST のかわりに markdown を docutils の入力に使うことができます。
docutils は html や latex, Open Document (.odt) などの出力に対応しているので、
組み合わせることで markdown から様々なフォーマットへ出力することができるようになります。

remarkdown のインストール

remarkdown は PyPI にパッケージがアップロードされていないため、簡単にインストールする方法がありません。
そのため、次の方法でインストールを行います。

まず、pip を用いて github のアーカイブ経由でパッケージをインストールします。

$ pip install https://github.com/sgenoud/remarkdown/archive/master.zip

なお、setup.py に不備があるので*1、ファイルをひとつ手動でダウンロードする必要があります(パスは各自適当な場所に読み替えてください)。

$ curl -LO https://raw.githubusercontent.com/sgenoud/remarkdown/master/remarkdown/markdown.parsley
$ mv markdown.parslay lib/python2.7/site-packages/remarkdown

remarkdown を使ってみる

remarkdown をインストールすると、一緒に md2html, md2latex, md2xml などのコマンドも一緒にインストールされます。
これらのスクリプトは markdown を入力として docutils を呼び出しています。

$ md2html test.md > test.html

Sphinx meets remarkdown

さて、ここからが本題です。
docutils と remarkdown の組み合わせができることを確認したところで、
今度は Sphinx と remarkdown を組み合わせてみることにします。

Sphinx を書き換える

Sphinx は docutils を利用して文書の変換を行っているのですが、
内部では reST 以外を想定していない作りになっています。

具体的には sphinx/environment.py の BuildEnvironment#read_doc() の中にこんな記述があります。

        pub = Publisher(reader=SphinxStandaloneReader(),
                        writer=SphinxDummyWriter(),
                        source_class=SphinxSourceClass,
                        destination_class=NullOutput)
        pub.set_components(None, 'restructuredtext', None)

Publisher のコンポーネントとして 'restructuredtext' を固定値で指定していますね。
入力フォーマット(パーサ)もここで決定しているので、markdown を入力として利用するには Sphinx を書き換える必要があります。

また、remarkdown は section ノードに必要な属性(ids, names)を正しくセットしていないので、
Sphinx を書き換える際についでに補正します。

Sphinx を書き換える、と言っても本体のソースコードを書き換えてしまうと、他の環境で使えなくなってしまうので、
conf.py の中から次のように書き換えを行います。

from docutils.core import Publisher


class MarkdownPublisher(Publisher):
    def __init__(self, *args, **kwargs):
        Publisher.__init__(self, *args, **kwargs)

        # replace parser FORCELY
        from remarkdown.parser import MarkdownParser
        self.reader.parser = MarkdownParser()

    def publish(self):
        Publisher.publish(self)

        # set names and ids attribute to section node
        from docutils import nodes
        for section in self.document.traverse(nodes.section):
            titlenode = section[0]
            name = nodes.fully_normalize_name(titlenode.astext())
            section['names'].append(name)
            self.document.note_implicit_target(section, section)


# replace Publisher
import sphinx.environment
sphinx.environment.Publisher = MarkdownPublisher

また、conf.py を編集する際、ソースの拡張子の設定もついでに書き換えておくとよいでしょう。

source_suffix = '.md'

Sphinx + remarkdown で文書を書く

さて、これで Sphinx と remarkdown を組み合わせて文章を書くことができるようになりました。

index.md に適当な markdown を入れて make html や make latexpdfja してみてください。
ちゃんと markdown を解釈して、HTML や PDF に変換してくれますね。

問題点

なんとかうまく動くようになったわけですが、まだいくつか問題があります。

  • remarkdown は github flavor を解釈しない
  • HTML を埋め込むとうまく解釈できない / 変換できない
  • toctree ディレクティブがないので、複数ファイルに分割できない

さて、どうしたらいいものやら…

まとめ

remarkdown を使って docutils を拡張したり、Sphinx を拡張したりしてみました。
実験する範囲ではそれっぽく動くものの、ちゃんとドキュメントをつくり上げることを考えると
乗り越え無くてはならない課題はまだいくつか残っているようです。

僕は markdown があまり好きではないのでここで筆を置きますが、
興味がある人は続きにトライしてみてはいかがでしょうか。

*1:記事で紹介している markdown.parsley が入らない問題の他、entry_points がうまく定義出来ていないおかげで setup.py install を実行しても md2* コマンド群がインストールされない問題などがあります。

flake8 の NOQA コメントは無敵ではない

flake8 のドキュメントを読むと、
警告を抑止する NOQA コメントについて

lines that contain a # noqa comment at the end will not issue warnings.
(訳: 末尾に # noqa というコメントがある行は警告されません)

と説明されています。

しかし、実際にはすべての警告を抑止できるわけではありません。
例えば、インデントのスペースの数を数える警告(E111)は NOQA コメントの効果がありません。

$ cat test.py
name = 'Taro'
if name:
  print 'Hello,', name  # noqa
else:
  print 'Hello world'   # noqa

$ flake8 test.py
test.py:3:3: E111 indentation is not a multiple of four
test.py:5:3: E111 indentation is not a multiple of four

では、具体的にどういったケースで NOQA コメントが有効で、どういうケースには効かないのでしょうか。

答えは pep8 パッケージのリファレンスにありました*1
Error codesの備考のところに次のように記載があります。

(^) These checks can be disabled at the line level using the # noqa special comment. This possibility should be reserved for special cases.
(訳: (^) マークのついている警告は # noqa スペシャルコメントにより行レベルでのチェックが無効になります。この機能は特殊なケースのために予約されています。)

エラーコード一覧を見ていくと (^) マークが付いている項目は pep8 が検出する警告の半分以下です。

じゃあどうするのさ

PEP8 守れ。以上。

困っていること

こういうデータを記述した時に、見やすさを維持しつつ flake8 に怒られない方法を知りたいです。

 data = [('Tokyo',    5),   # noqa
         ('Kanagawa', 10),  # noqa
         ('Saitama',  8),   # noqa
         ('India',    20)]  # noqa

まとめ

NOQA コメントが万能じゃないことがわかりました。
オチはない。

*1:flake8 は pep8 パッケージを利用して pep8 のチェックをしています

Re: Re: SphinxのLaTeXのフォーマットをいじる

先日、Re: SphinxのLaTeXのフォーマットをいじる の記事を書いた直後に、
sphinx-users ML に独自のclsファイルを使用する方法がポストされました。

前回の記事では conf.py でプリアンブル部にゴリゴリ定義を突っ込んでいましたが、

  • Python の文字列として記述するため、エスケープを気にする必要がある
  • ファイル内にPython のコードと LaTeX マクロがミックスされるため、読み書きしづらい
  • エディタのサポートが得られない

などの問題があるので、今回はこの「独自の cls ファイルを使用する」方法を使ってみることにします。

自分だけの最強の cls を作る

とは言え、いちからドキュメントクラスを書き下すことは難しいので、
既存のドキュメントクラスをベースにカスタマイズしていくことにします。

まずは jsarticle をベースにすることにします。
これまでは conf.py の latex_docclass と latex_documents を使ってクラスを選択していましたが、
独自に cls ファイルを定義する場合は \LoadClass マクロを使います。

\LoadClass{jsarticle}

例えば、行間を広げるようなドキュメントクラスを作ってみます。
次の内容のファイルを myjsarticle.cls というファイル名で保存します。

\LoadClass{jsarticle}

\renewcommand{\baselinestretch}{2.0}


そして、conf.py に次のように記述します。

latex_additional_files = ['myjsarticle.cls']

latex_documents = [
  ('index', 'test.tex', u'test Documentation',
   u'test', 'myjsarticle'),
]

sphinxhowto, sphinxmanual をベースにする

前回の記事では sphinxhowto と jsarticle を組み合わせて、必要な箇所だけ書き換えるようにしていたので
今度は sphinxhowto をベースにしてみましょう。
sphinxhowto (sphinxmanual) はちょっと変わった形をしたドキュメントクラスです。

  • ベースになるのは article や manual などの基本のドキュメントクラス
  • ベースのドキュメントクラスの定義を一部差し替えている
  • ベースのドキュメントクラスは差し替えしやすいよう、\sphinxdocclass という変数名経由で指定

そのため、sphinxhowto (sphinxmanual) を \LoadClass する前に \sphinxdocclass という変数を定義します*1
今回は jsarticle を指定します。

\def\sphinxdocclass{jsarticle}
\LoadClass{sphinxhowto}

\renewcommand{\baselinestretch}{2.0}

そして、次に conf.py を書き換えます。

latex_additional_files = ['myjsarticle.cls']

latex_documents = [
  ('index', 'test.tex', u'test Documentation',
   u'test', 'myjsarticle'),
]

まとめ

  • latex_additional_files と latex_documents の設定によって、独自のドキュメントクラスを定義することができた
  • \LoadClass を使うことで既存のドキュメントクラスを読み込むことができ、必要な部分だけ差し替えられるようになった
  • sphinxhowto の部分差し替えもできるようになった

*1:独自のドキュメントクラスを利用する場合、Sphinx は \sphinxdocclass の初期値に conf.py の latex_docclass['manual'] を設定するので conf.py 側でコントロールすることもできます

pandoc + sphinx + transifex で既存ドキュメントの翻訳を行う

この間の Sphinx+翻訳 Hack-a-thon で話題に上がった件について調査してみた。

お題は以下のとおり。

  • 元の文書は markdown で記述されている
  • Sphinx を使って翻訳してみようと思っている
  • 元の文書が更新された時に差分管理できるとよい

その場で出た案はタイトルの通りで、

  • pandoc で reST 形式に変換して
  • Sphinx を使って gettext 化して
  • sphinx-intl を使って transifex にアップロードして
  • transifex で翻訳をすると良さそう

というもの。
gettext 化を挟むことで、翻訳しやすく、また原文の変化を追いかけやすいそうな。

というわけで、実際に試してみました。

環境を作る

お題は redis-doc

まずはワークスペースを作る。

$ mkdir redis-doc
$ cd redis-doc
$ git init
$ virtualenv .
$ . bin/activate
$ pip install --pre sphinx sphinx-intl transifex-client
$ pip freeze > requirements.txt
$ rehash
$ git submodule add https://github.com/antirez/redis-doc

virutalenv で環境を作って、その中に Sphinx, sphinx-intl, transifex-client を入れてます*1
そして、翻訳対象の文書を git の submodule として管理します。

Sphinx 環境を作る

つづいて Sphinx 環境を作ります。
translated ディレクトリ以下にプロジェクトを作ります。

$ sphinx-quickstart translated
$ vi translated/source/conf.py
language = 'ja'
locale_dirs = ['locale/']
gettext_compact = False
$ vi Makefile
html:
        tx pull -l ja
        sphinx-intl build --pot-dir build/locale --locale-dir source/locale
        $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
$ cat > .gitignore
bin/
include/
lib/
translated/build
^D
$ git add .
$ git commit -m 'Initial commit'

conf.py では language, locale_dirs, gettext_compact の設定をしています。翻訳する上で必要最低限の設定です*2

また、Makefile の html ターゲットには tx pull と sphinx-intl build の実行を追加しています。
make html する際に transifex から最新の翻訳カタログを取ってくるようにするものです。
しばらく時間を開けるとコマンドを忘れてしまいがちなので、忘れないように記述を追加しておきます。

pandoc, transifex を使うための準備

続いて transifex 上に(アカウントと)プロジェクトを作成します。
サイトにアクセスすると大体わかると思うので説明は省略します。

そして、transifex-client(txコマンド)を使って transifex にアップロードする準備をします。

$ cd translated
$ tx init
$ echo 'type = PO' >> .tx/config
$ cd ..

transifex ディレクトリにて tx init を実行して、設定ファイルを生成しておきます。
また、0.11系では .tx/config ファイルに type オプションが指定されていないので、書き加えておきます。

最後に markdown ファイルを pandoc で reST に変換して、transifex にアップロードするためのスクリプトを用意します。

#!/bin/bash

TRANSIFEX_PROJECT_NAME=redis-doc

mkdir -p translated/source/commands
mkdir -p translated/source/topics

cd redis-doc
find . -name "*.md" -print0 | while read -r -d '' md;
do
    CONVERTED="../translated/source/${md%.*}.rst"
    echo -n "Translating $md ... "
    if [ "$md" -nt "$CONVERTED" ]; then
        pandoc "$md" -o "$CONVERTED"
        touch -r "$CONVERTED" "$md"
        echo "done."
    else
        echo "skipped."
    fi
done

find . \( -name "*.jpg" -or -name "*.png" -or -name "*.gif" \) -print0 | while read -r -d '' img;
do
    COPIED="../translated/source/$img"
    echo -n "Copying $img ... "
    if [ "$img" -nt "$COPIED" ]; then
        cp -a "$img" "$COPIED"
        echo "done."
    else
        echo "skipped."
    fi
done


cd ../translated
make html
sphinx-intl update --locale-dir source/locale --pot-dir build/locale
sphinx-intl update-txconfig-resources --transifex-project-name=$TRANSIFEX_PROJECT_NAME --locale-dir source/locale --pot-dir build/locale
tx push -s

このスクリプトを実行すると、

  • pandoc を使って markdown ファイルを reST 形式に変換して translated/ ディレクトリに配置
  • 画像類も適当にコピー
  • Sphinx を使って gettext 化を実施
  • sphinx-intl を使ってリソース登録
  • transifex-client (txコマンド) を使って、翻訳カタログを transifex に転送

という一連の手順を実行します。

transifex で翻訳する

ひたすら翻訳してください。

HTML に変換する

transifex で翻訳が一息ついたら HTML に変換してみましょう。

変換する前に、目次となる index.rst を作っておきます。:glob: オプションを使って簡易的に並べています。

Redis Documentation (Japanese)
===============================

.. toctree::
   :glob:

   README
   topics/*
   commands/*

準備段階で Makefile をいじっているので make html を実行するだけで ok です。

$ cd translated
$ make html

元の文章が新しくなった場合は submodule を更新し、スクリプトを再実行すれば ok です。

この方法の問題点

この方法を試したところ、いくつか問題がありました。

  • pandoc で適切に変換できない文章がある
    • HTML から変換する際など、うまく変換できないことがありました
    • redis-doc では見出しのないファイルがあるため、HTML に変換するときに toctree に含まれないという問題がありました
  • sphinx-intl はファイル名に空白を含んでいるとうまく扱えない
    • あとで pull request 送ります…

まとめ

Sphinx ではない形式で書かれたドキュメントについても、pandoc を使うことで Sphinx の翻訳フローに乗せることができます。
いくつか問題点があるなど、まだ完璧なフローとはいえませんが、
うまくアレンジしていくと手に馴染んだツールで翻訳を行うことができそうです。

*1:transifex-client は最新のβ版(0.11.1.beta)でないとまともに動かないので --pre オプションを指定しています

*2:language は sphinx-build 実行時に指定する方法もありますが、ここでは簡略化して conf.py に記述しました

Re: SphinxのLaTeXのフォーマットをいじる

卒論とか修論TeX で書いていた割には、TeX マクロが苦手な @tk0miya です。
先輩から秘伝のタレ的?なスタイルファイルを頂いたのでそれで済ませてました。

Sphinx で PDF 出力するときは TeX を介して出力することが多いのですが、
未だに TeX マクロをちゃんと理解していないのでまったくいじらず、素のままで使っていました。

そんな折、
SphinxのLaTeXのフォーマットをいじる という記事を見かけました。

この記事では Sphinx 本体や付属のマクロをいじっているのですが、Sphinx は設定でアプローチできるようになっているので、
同じことを conf.py の記述だけで実現してみようと思います。

用紙サイズを a4 に変更する

latex_elements['papersize'] 経由で変更できます。

latex_elements = {
    'papersize': 'a4paper',
}

latex_paper_size という変数もあるので、こっちで指定してもよいみたい。こっちは page と size のあいだにアンダースコアが入る。
その場合は paper を付けずに指定する模様。

latex_paper_size = 'a4'

article クラスを利用する

latex_documents の末尾の要素を書き換えます。デフォルトでは report 系を意味する 'howto' が指定されています。

latex_documents = [
  ('index', 'test.tex', u'test Documentation',
   u'test', 'howto'),
]

行間を狭くする

BEGIN_DOC の内容はいじれませんが、マクロの差し替えをするだけであれば latex_elements['preamble'] を使います。

latex_elements = {
    'preamble': '''
\\renewcommand{\\baselinestretch}{0.8}
'''
}

default_elements を書き換える

conf.py の latex_elements を設定しておくと、上書きされます。
記事と同じ内容に揃えるのであればこうすると良いみたい。

latex_elements = {
    'papersize': 'a4paper',
    'fontpkg': '',
    'fncychap': '',
}

ちなみに、fncychap は language = 'ja' の設定をしていると、自動的に空になるようです。

TeX のドキュメントクラスに jsarticle, jsbook を使いたい

これは LaTeX 経由での PDF 出力の設定でもお馴染みですね。latex_docclass を定義します。
普段は jsbook しか設定していませんが、ちゃんと jsarticle も使えるようにしておきます。

latex_docclass = {
    'howto': 'jsarticle',
    'manual': 'jsbook',
}

ページ番号を表示する

これもプリアンブル部に定義すると良いので、latex_elements['preamble'] に書き加えます。

latex_elements = {
    'preamble': '''
\\pagestyle{plain}
\\thispagestyle{plain}
'''
}

ここでは \thispagestyle{empty} を無効にするために、再度 \thispagestyle を呼んでページスタイルを上書きしています。

ページ番号の深さを変更する

これもやっぱりプリアンブル部です。

latex_elements = {
    'preamble': '''
\\setcounter{secnumdepth}{3}
'''
}

タイトルを変更する

ちょっと量が多いので悩んだのですが、これもプリアンブル部に突っ込みます。
TeX のエラーで悩まされたので \makeatletter, \makeatother で囲んで調整しています。

latex_elements = {
    'preamble': '''
\\makeatletter
\\renewcommand{\maketitle}{
  \\ifsphinxpdfoutput
    \\begingroup
      \\def\\\\{, }
      \\def\\and{and }
      \\pdfinfo{
        /Author (\\@author)
        /Title (\\@title)
      }
    \\endgroup
  \\fi
  \\begin{center}
    \\sphinxlogo%
    {\\Large \\@title} \\par
  \\end{center}
  \\begin{flushright}
    \\@date \\hspace{3zw} \\@author \\par
    \\py@authoraddress \\par
  \\end{flushright}
  \\@thanks
  \\setcounter{footnote}{0}
  \\let\\thanks\\relax\\let\\maketitle\\relax
}
\\makeatother
'''
}

目次を変更する

これもやっぱりプリアンブル部。

latex_elements = {
    'preamble': '''
\\let\\pyOldTableofcontents=\\tableofcontents
\\renewcommand{\\tableofcontents}{
  \\begingroup
  \\parskip = 0mm
  \\pyOldTableofcontents
  \\endgroup
  \\vspace{12pt}
}
'''
}

元の定義だと \py@OldTableofcontents って変数名なんだけど、なぜかエラーになったので @ を取ってしまいました。
原因がよく分からないけど、動いたので深追いせず。

まとめ

プリアンブル部をいじりまくってるので、最終的にどうなったのかを貼っつけて終わりにします。

language = 'ja'

latex_elements = {
    'papersize': 'a4paper',
    'fontpkg': '',
    'fncychap': '',
    'preamble': '''
\\renewcommand{\\baselinestretch}{0.8}
\\pagestyle{plain}
\\thispagestyle{plain}
\\setcounter{secnumdepth}{3}

\\makeatletter
\\renewcommand{\maketitle}{
  \\ifsphinxpdfoutput
    \\begingroup
      \\def\\\\{, }
      \\def\\and{and }
      \\pdfinfo{
        /Author (\\@author)
        /Title (\\@title)
      }
    \\endgroup
  \\fi
  \\begin{center}
    \\sphinxlogo%
    {\\Large \\@title} \\par
  \\end{center}
  \\begin{flushright}
    \\@date \\hspace{3zw} \\@author \\par
    \\py@authoraddress \\par
  \\end{flushright}
  \\@thanks
  \\setcounter{footnote}{0}
  \\let\\thanks\\relax\\let\\maketitle\\relax
}
\\makeatother

\\let\\pyOldTableofcontents=\\tableofcontents
\\renewcommand{\\tableofcontents}{
  \\begingroup
  \\parskip = 0mm
  \\pyOldTableofcontents
  \\endgroup
  \\vspace{12pt}
}
''',
}

latex_docclass = {
    'howto': 'jsarticle',
    'manual': 'jsbook',
}

latex_documents = [
  ('index', 'test.tex', u'test Documentation',
   u'test', 'howto'),
]

勉強になりました。

Pillow の ImageDraw#textsize() が間違った値を返している件

Pillow 2.2.0 以降には ImageDraw#textsize() にバグがあり、TrueType フォントを指定していると正しい高さが返りません。

どういう問題なのか

テキストとそれを囲む四角を描画する次のコードを実行してみます。

from PIL import Image, ImageDraw, ImageFont

image = Image.new('RGB', (512, 256), (255, 255, 255))
drawer = ImageDraw.Draw(image)
font = ImageFont.truetype('/Library/Fonts/Osaka.ttf', 32)

# drawing text
STRING = 'Hello, python language!'
drawer.text((10, 10), STRING, fill='black', font=font)

# drawing rectangle surrounding text
size = drawer.textsize(STRING, font=font)
drawer.rectangle((10, 10, 10 + size[0], 10 + size[1]), outline='black')

image.save('example.png', 'PNG')

すると次のような図が出力されます。
f:id:tk0miya:20140712002748p:plain
カンマや y, g と言った文字の下辺が枠の外から飛び出しています。

この問題は TrueType フォントを表す PIL.ImageFont.FreeTypeFont クラスが文字の大きさを返す際に
offset 値を含めないサイズを返してしまっているために発生しています。

この問題はすでに github 上では修正されているのですが、
現在リリースされている Pillow-2.5.1 ではこの問題は解決されていません。

回避策

Pillow-2.2.0 以降には FreeTypeFont クラスに getoffset() というメソッドがあるので、この値を用いて補正します。

どのバージョンの Pillow でも動くようにするには以下のようなコードを書きます*1

from PIL import PILLOW_VERSION
from PIL.ImageFont import FreeTypeFont

def textsize(drawer, text, font=None):
    size = drawer.textsize(text, font)
    # Avoid offset problem in Pillow (>= 2.2.0, < 2.6.0)
    if isinstance(font, FreeTypeFont) and "2.2.0" <= PILLOW_VERSION < "2.6.0":
        offset = font.getoffset(text)
        size = (size[0] + offset[0], size[1] + offset[1])

    return size

まとめ

しばらくはバグったままなので、adhoc なコードを使って回避して生きていきましょう。

懺悔

このバグは日本語フォントが欠ける問題を直したところに、他の人が offset 値の調整を加えることで発生しているので
僕は(間接的ではあるものの)バグの生みの親ということになります。

大変申し訳無いです。

*1:まだ 2.6.0 はリリースされていませんが、次のリリースで直る想定で書かれています

image/figure ディレクティブのオプションまとめ

最近、Sphinx や docutils の blockdiag ディレクティブのオプションを見なおし、
reST 標準の image/figure ディレクティブと同じオプションを受け取るようにしました。
:width: や :height:、:scale: などのオプションが使えるようになっています。

その際にみつけた image ディレクティブのオプションの分かりづらい動作についてまとめてみます。

:width:, :height:, :scale: の関係

:width:, :height: そして :scale: は 3つでひとそろいのオプション群です。
ほとんどのケースではどれかひとつだけを使うようですが、
これらのオプションは組み合わせ使うととても分かりづらい動きをします。

:width: のみ 横幅を指定。縦幅はアスペクト比から算出。
:height: のみ 縦幅を指定。横幅はアスペクト比から算出。
:scale: のみ 拡大率を指定。縦幅、縦幅は元画像の大きさから計算。
:width: + :height: 縦幅、横幅を指定。アスペクト比は維持されない。
:width: + :scale: 横幅は width 値 x scale (拡大率)となる。縦幅はアスペクト比から算出。
:height: + :scale: 縦幅は height 値 x scale (拡大率)となる。横幅はアスペクト比から算出。
:width: + :height: + :scale: width 値 x scale、height 値 x scale の大きさになる。アスペクト比は維持されない。

例えば

.. image:: logo.png
   :width: 200px
   :scale: 50%

という指定では横幅は 200px * 50% = 100px となり、縦幅はアスペクト比から算出されます。

自分でも実装してみたものの、組み合わせてどう使うのかイマイチ分からないですね。

HTML と LaTeX で異なる :width: オプションの指定の仕方

Sphinx は "one source, multiple output format" みたいなところがあるので、
reST で書いておけばいろんなフォーマットに使えると考えがちですが、
:width: オプションはその例外のひとつです。

例えば

.. image:: logo.png
   :width: 200px

と指定した場合、HTML では 200px に縮小(拡大)して画像が表示されますが、
LaTeX では原寸のまま画像が埋め込まれます。

これは HTML と LaTeX で長さの単位が異なるために起きるもので、
LaTeX では px での width, height 指定は無視されます。
LaTeX では width の単位として cm や pt、em を利用します*1が、こちらは HTML でも利用できます。

HTML LaTeX
(単位なし) ×
px ×
% ×
pt
cm
in
em

というわけで、:width: や :height: を指定するときは単位を意識しておくと幸せになれるかもしれません。

訂正 (7/11 11:55): LaTeX では % 指定は無効でした。

image と figure で align の取る値が異なる

image ディレクティブでは left, center, right, top, middle, bottom の 6種類が指定できるのに対して、
figure ディレクティブでは left, center, right の 3種類だけ受け付けています。

:figwidth: オプションの image の謎

figure ディレクティブには :figwidth: オプションというオプションがあります。
figure ブロックの幅を指定するためのオプションです*2

この :figwidth: オプションは :width: オプションと同様に幅をしていするのですが、
特別なオプションとして image を指定することができます。

この リファレンスでは次のように説明されています。

A special value of "image" is allowed, in which case the included image's actual width is used (requires the Python Imaging Library).

画像の幅を計測して、その値を使うという指定です。

このオプションはあまり使われていないのか、実際に呼び出してみるとうまく動かないというバグが有ります(trunk では直っています)。

しかし、このオプション "image's actual width" と書いてあるとおり、
画像の実寸を使っているので :width: オプションや :scale: オプションと組み合わせると
やたらと大きい(小さい) figure ブロックができるため、どう使うと良いのかというのが難しい指定です。

このオプション、どういう時に使うと便利なんですかねえ。

まとめ

使ったことのないオプションが多い割に、互換性を保とうとすると面倒なことが多くて辛いです。

*1:em は 幅の単位なので、height には指定できません。詳しくは TeX の記述を調べてみてください。

*2:figure ブロックについては リファレンス の図示がわかりやすいです