LuaTeXの普及状況をLuaTeXで調べてみた

2019-12-23 (updated: 2025-12-09) #LuaTeX #Lua #PDF

本稿はTeX & LaTeX Advent Calendar 2019の23日目の記事です。22日目はpuripuri2100さんでした。24日目はgolden_luckyさんです。

2016年にLuaTeX 1.0がリリースされてからおよそ3年が経ち、体感ではありますが私の周辺ではLuaTeXの存在感は徐々に増してきているように思われます。LuaTeXが日本で広まっていく上での障壁として「日本語の文献が少ない」ということがかねてから指摘されていましたが、定期的に開催される技術書典でLuaTeXをテーマとする同人誌が複数頒布されてきたこと、恒例のTeX & LaTeX Advent Calanedarの重点テーマに2年連続でLuaTeXが採択されたことなどがあり、LuaTeXに関する日本語の解説もその量が増えてきました。またLaTeX文書クラスもltjs系クラスbxjs系クラスに加えてjlreqクラスとLuaTeXに対応しているものの選択肢が増えてきており、活躍の幅も広がっています。本稿を読んでいる皆さんも、一度はお使いになったことがあるでしょうか?

今回はそんなLuaTeXが現在どのぐらいのシェアを誇るのか、調査することを目標にしてみたいと思います。LuaTeXのシェアを調査するといいましたが、実はこれはそんなに簡単なことではありません。世界中でLuaTeXが実行される回数を厳密にカウントすることはもちろん不可能ですし、周囲への聞き取り調査やTwitterアンケートを行ってもデータが偏ってしまうでしょう。一般的なソフトウェアであれば、ダウンロード数からシェアを予測するという方法もありますが、TeXの世界はというと、様々なTeX処理系が全部入りのTeX Liveフルスキームをインストールするのが一般的であり、ダウンロード数からシェアを推測するという手は使えません。

どのような方法を採るにせよバイアスを完全に取り除くのは困難なので、今回は開き直って、敢えて偏った対象を選んでシェアを計算してみたいと思います。具体的には、世界のTeX系開発者におけるLuaTeXのシェアに焦点を絞ります。これであればTeX Liveに含まれる処理系や文書クラス、パッケージ等々のドキュメントの組版に用いられている各TeX処理系の割合を求めることで、大勢を見て取ることができそうです。TeX Liveに成果物を上げている開発者の中での処理系シェアを知ることは、今後のTeX界の動向を予測する上でも役立つでしょう。

PDFの生成プログラムを推測する

TeX Liveには各パッケージのプログラム本体とは別に、付属ドキュメントが含まれています。こうしたドキュメントの中にはTeX組版とは無関係のテキストファイル(例えばREADME)も多数含まれていますが、今回はそうしたものは対象としないことにし、最終的にPDFとなっているドキュメントのみをシェア計測の対象にします。対象をTeX Liveに含まれるPDFドキュメントに絞ると、そのほとんどはTeX組版されたものということになります。

TeX Liveには最終成果物であるPDFの他に、それらのソース(主にTeXファイル)も含まれているのが通常です。そのため、PDF生成に用いられているプログラム(TeX処理系など)を推測する方法は、ソースから推測するアプローチとPDFから推測するアプローチの2通りが考えられます。TeXソースから処理系を推測することについては先行事例もありますが、これは意外と大変です。場合によってはLaTeXのクラスオプションや使用パッケージから推測できる場合もありますが、必ずしも自明ではありません。例えば次のLaTeX文書はほとんどあらゆるTeX処理系でコンパイル可能なため、作者でもない限り何で組版すべきTeXソースなのかはわかりません。

\documentclass{article}
\begin{document}
Hello {\TeX}!
\end{document}

TeXソースとPDFの双方が入手可能な場合、実は組版結果のPDFを見た方が生成プログラムの推測は簡単です。PDFには、文書本体には印字されない「文書プロパティ」と呼ばれるデータが含まれており1、文書の作成者、タイトル、キーワードなどの情報を埋め込んでおくことができます。LaTeXを用いてPDFを作成する場合、何らかのパッケージ2を用いて明示的に値を指定しない限り、これらのほとんどの値は空のままになるため、実際には設定されていないことがほとんどです。しかし、幸いにも“PDF Producer”というPDF生成を行ったプログラムの情報を格納する項目は、多くのPDF生成ツールが規定で値を挿入するため、ほとんどのTeX製PDFにこの情報が含まれています。

試しに、上記の単純なLaTeX文書をさまざまなTeX処理系でコンパイルして、出来上がったPDFをAcrobat Readerで開いて文書プロパティを確認してみましょう3。以下に、手許のTeX Live 2019で試した結果を載せておきます。

【LuaTeX製PDFの文書プロパティ】

LuaTeX製PDFの文書プロパティ

LuaTeXでPDFを作成する場合、LuaTeX自体がPDFの出力まで自力で行うため、“PDF Producer”はLuaTeX-<version>の形で埋め込まれていました。その上の“Application”項目は単にTeXとなっています。

【XeTeX製PDFの文書プロパティ】

XeTeX製PDFの文書プロパティ

XeTeXでPDFを作成する場合は、直接的にはXeTeX向けに拡張されたdvipdfmxであるxdvipdfmxがPDF生成を担うため“PDF Producer”はxdvipdfmx (<version>)の形式になっています。XeTeXの場合は“Application”項目もXeTeX output <date>という独自の値になっているようです。

【pdfTeX製PDFの文書プロパティ】

pdfTeX製PDFの文書プロパティ

pdfTeXはもちろん自前でPDFを出力する機能を備えていますので、“PDF Producer”はpdfTeX-<version>の形です。“Application”項目もLuaTeX同様単にTeXとなっています。つまり、残念ながら“Application”項目の値を利用してPDF生成に用いられたプログラムを判定するのは難しそうです。

【pTeX + dvipdfmx製PDFの文書プロパティ】

pTeX + dvipdfmx製PDFの文書プロパティ

pTeXの場合は、オリジナルTeXと同様、出力はPDFではなくDVIなので、別のツールを用いてPDFに変換してやる必要があります。DVI → PDF変換ルートは複数ありますが、近年は(少なくとも日本では)dvipdfmx4 を用いるのが一般的なので、この例でもそのフローを採用しています。

以上から、PDF文書プロパティの“PDF Producer”を見ることにより、少なくとも標準的な方法で作成されたPDFであれば、どのTeX処理系を用いて作成したものか推定できるということがわかりました。もちろん、このアプローチは完璧というわけではなく、dvipdfmxで変換されたPDFの変換元DVIがどのTeX処理系で生成されたものかを推定することは困難です5。ただ、今回はLuaTeXのシェアを調べることが目的なので、dvipdfmx経由で生成されたPDFについてはそれ以上の生成ルートを追究しなくてもよいでしょう。古いPDFについてはdvipdfmxではなくdvipdfmで生成されたものもありますが、話が複雑になりそうなのでdvipdfmとdvipdfmxも区別しないことにします。

また、例えば次のようなケースでも推定に失敗し得ます:

  1. 未知のツールを用いてPDF生成が行なわれている場合
  2. 一旦PDFを生成したあとに、他のツール(例えばAcrobat Distiller)で変更を加えてある場合
  3. 何らかの方法でProducer項目を書き換えてしまっている場合

しかし、TeX Liveに含まれるPDFの大半は数種類のTeX処理系を用いた標準的な生成フローで作られたものなので、上記の1. や2. に該当するようなPDFは少数派です。そのため、今回はこれらは「その他 (Others)」としてまとめてしまってもさほど問題はないでしょう。また、わざわざ3. のようなことをして、PDF Producerを偽装(?)するような人もあまりいないでしょうから、特段考慮しなくても大丈夫かと思います。

さて、PDFの文書プロパティを見ると、生成に用いられたTeX処理系が概ね判定できることがわかりました。ではどうやってTeX Liveに含まれる多数のPDFファイルからそうした情報を収集し、解析するのがよいでしょうか? 実はLuaTeXにはPDFを扱うためのライブラリpdfeが標準で搭載されており、LuaTeXが内蔵するLua処理系から利用することが可能です。このライブラリについては別途Qiitaにチュートリアル記事を投稿しましたので、興味のある方は参照してください:

TeX LiveドキュメントにおけるLuaTeXのシェア

それではいよいよTeX Liveのドキュメントツリー (TEXMF/doc) にあるPDFファイル群における各TeX処理系のシェアを、LuaTeXを用いて調べていきましょう。まずは必要なライブラリを読み込みます。ついでに例外処理で使用するため警告メッセージを標準エラー出力に吐き出す関数も定義しておきます。

local kpse = require 'kpse'
local pdfe = require 'pdfe'
local lfs = require 'lfs'

function warn(msg, ...)
  io.stderr:write('WARNING: ' .. msg:format(...) .. '\n')
end

続いてLuaFileSystem (lfs) の力を借りつつ、指定したディレクトリ以下のPDFファイルを列挙する配列を返す関数を定義します。ただしTeX Liveのドキュメントツリーでmanディレクトリ以下にあるPDFはいわゆるmanpages用のroff文書を機械的に変換して生成されたもので、各パッケージの作者が彼ら自身の手でTeX組版したものではありませんので、今回の調査では除外します。

function list_pdf(dir, list)
  list = list or {}  -- 引数に与えられたリストを用いるか、新規に作成する
  
  for entry in lfs.dir(dir) do
    if entry ~= '.' and entry ~= '..' and entry ~= 'man' then  -- manディレクトリは無視
      local ne = dir .. '/' .. entry
      if lfs.attributes(ne).mode == 'directory' then
        list_pdf(ne, list)
      else
        if ne:match('%.pdf$') then  -- PDFファイルだけを収集
          table.insert(list, ne)
        end
      end
    end
  end
    
  return list
end

文書プロパティの“PDF Producer”エントリにある文字列から使用されたTeX処理系(またはDVIウェア)を推定する関数judge_producer()を定義します。先述したとおり、今回はLuaTeXのシェアを調べることが主目的なので、DVIを経由して生成されたPDFについては大雑把にしか分類しません。この関数のロジックは実際にTeX Liveに含まれるPDFドキュメントの“PDF Producer”値を見ながら泥縄式に考案したもので、汎用的とは言い難いです。ひとまず今回の調査には十分ということで、細かいことは気にしないでください。

function judge_producer(p)
  local res = 'Others'  -- とりあえず「その他」に初期化

  -- 正規化
  p = p:lower()
  if p:match('^miktex') then  -- MikTeX搭載のTeX処理系では特殊な値になる
    p = p:sub(8)
  end

  -- PDF生成ツールの推定
  if p:match('^luatex') then
    res = 'LuaTeX'
  elseif p:match('^xdvipdfmx') or p:match('^xetex') then
    res = 'XeTeX'
  elseif p:find('pdftex') or p:find('pdflatex') or p:match('^pdfetex') then
    res = 'pdfTeX'
  elseif p:match('^dvips') or p:find('ghostscript') then
    res = 'Ghostscript'
  elseif p:match('^dvipdfm') then
    res = 'dvipdfmx'
  end
  return res
end

あとはpdfeライブラリを用いてlist_pdf()関数で取得した各PDFについて文書プロパティの“PDF Producer”を読み出し、その値をjudge_producer()に流し込んで、各TeX処理系のシェアを集計すればOKです。稀にpdfeでうまく開けないPDFファイルやなぜか文書プロパティが存在しないPDFがあるので、適当に例外処理をしておきます。

-- 引数doc_dirで指定したディレクトリ以下のPDFファイルについてシェアを計算
function calc_share(doc_dir)
  local share = {
    ['LuaTeX'] = 0,
    ['XeTeX'] = 0,
    ['pdfTeX'] = 0,
    ['Ghostscript'] = 0,
    ['dvipdfmx'] = 0,
    ['Others'] = 0
  }

  -- doc_dir以下のPDFファイルのリストを取得し、ループを回す
  for _, fn in ipairs(list_pdf(doc_dir)) do
    -- PDFファイルを開く
    local doc = pdfe.open(fn)
    
    -- PDFファイルのopenに失敗する場合がある
    if pdfe.getstatus(doc) ~= 0 then
      warn('Cannot open File %s', fn)
    else
      -- PDFのinfo辞書を取得
      local info = pdfe.getinfo(doc)
      if info == nil then
        warn('No info in File %s', fn)
      else
        -- Producerエントリの値を読み出す
        local info_producer = pdfe.getstring(info, 'Producer')
        if type(info_producer) ~= 'string' then
          warn('No Producer entry in File %s', fn)
        else
          -- PDF生成プログラムを推定し、結果を集計
          local producer = judge_producer(info_producer)
          share[producer] = share[producer] + 1
        end
      end
    end
    
    -- PDFファイルは忘れずに閉じる
    pdfe.close(doc)
  end

  return share
end

これでほぼ完成です。せっかくなので、シェアの集計結果をグラフにして可視化するところまですべてLuaTeXでやってしまいましょう。PythonにmatplotlibあればLuaTeXにpgfplotsありです。しかもTeX的数式にネイティブ対応。というわけで、次のようなLuaLaTeX文書を作成しました。

%#!lualatex
\documentclass[border=4mm]{standalone}

% lua code
\usepackage{luacode}
\begin{luacode*}
local kpse = require 'kpse'
local pdfe = require 'pdfe'
local lfs = require 'lfs'

function warn(msg, ...)
  io.stderr:write('WARNING: ' .. msg:format(...) .. '\n')
end

function list_pdf(dir, list)
  list = list or {}
  
  for entry in lfs.dir(dir) do
    if entry ~= '.' and entry ~= '..' and entry ~= 'man' then
      local ne = dir .. '/' .. entry
      if lfs.attributes(ne).mode == 'directory' then
        list_pdf(ne, list)
      else
        if ne:match('%.pdf$') then
          table.insert(list, ne)
        end
      end
    end
  end
    
  return list
end

function judge_producer(p)
  local res = 'Others'

  -- nomarize
  p = p:lower()
  if p:match('^miktex') then
    p = p:sub(8)
  end

  -- judge
  if p:match('^luatex') then
    res = 'LuaTeX'
  elseif p:match('^xdvipdfmx') or p:match('^xetex') then
    res = 'XeTeX'
  elseif p:find('pdftex') or p:find('pdflatex') or p:match('^pdfetex') then
    res = 'pdfTeX'
  elseif p:match('^dvips') or p:find('ghostscript') then
    res = 'Ghostscript'
  elseif p:match('^dvipdfm') then
    res = 'dvipdfmx'
  end
  return res
end

function calc_share(doc_dir)
  local share = {
    ['LuaTeX'] = 0,
    ['XeTeX'] = 0,
    ['pdfTeX'] = 0,
    ['Ghostscript'] = 0,
    ['dvipdfmx'] = 0,
    ['Others'] = 0
  }

  for _, fn in ipairs(list_pdf(doc_dir)) do
    local doc = pdfe.open(fn)
    if pdfe.getstatus(doc) ~= 0 then
      warn('Cannot open File %s', fn)
    else
      local info = pdfe.getinfo(doc)
      if info == nil then
        warn('No info in File %s', fn)
      else
        local info_producer = pdfe.getstring(info, 'Producer')
        if type(info_producer) ~= 'string' then
          warn('No Producer entry in File %s', fn)
        else
          local producer = judge_producer(info_producer)
          share[producer] = share[producer] + 1
        end
      end
    end
    pdfe.close(doc)
  end

  return share
end

function print_ymax(share)
  ymax = 1000
  for _, v in pairs(share) do
    while v > ymax do
      ymax = ymax + 1000
    end
  end
  tex.sprint(ymax)
end

function print_order(share)
  local tab = {}
  local res = ''
  
  local function spairs(t, order)
    local keys = {}
    for k in pairs(t) do keys[#keys+1] = k end

    if order then
      table.sort(keys, function(a,b) return order(t, a, b) end)
    else
      table.sort(keys)
    end

    local i = 0
    return function()
      i = i + 1
      if keys[i] then
        return keys[i], t[keys[i]]
      end
    end
  end
  
  for k, v in pairs(share) do
    if k ~= 'Others' then
      tab[k] = v
    end
  end
  
  for k, _ in spairs(tab, function(t, a, b) return t[b] < t[a] end) do
    res = res .. string.format('\\pname{%s}, ', k)
  end

  tex.sprint(res .. '\\pname{Others}')
end

function print_values(share)
  local res = ''
  for k, v in pairs(share) do
    res = res .. string.format('(\\pname{%s}, %s) ', k, v)
  end
  tex.sprint(res)
end

local doc_dir = kpse.var_value('TEXMFDIST') .. '/doc'
share = calc_share(doc_dir)
\end{luacode*}

% pgfplots
\usepackage{pgfplots}
\pgfplotsset{compat=1.16}

% logos
\usepackage{bxtexlogo}
\bxtexlogoimport{*}
\makeatletter
\newcommand{\dvipdfmx}{dvipdfm(x)}
\newcommand{\pname}[1]{%
  \@ifundefined{#1}{#1}{\@nameuse{#1}}}
\makeatother

\begin{document}

\begin{tikzpicture}
\begin{axis}[
  ybar,
  ylabel={\#PDF},
  xlabel={PDF Producer},
  ymin=0,
  ymax={\directlua{print_ymax(share)}},
  xticklabel style={rotate=90},
  symbolic x coords/.expand once={%
    \directlua{print_order(share)}
  },
  nodes near coords, 
  nodes near coords align={vertical},
  axis lines*=left]
\addplot coordinates {
  \directlua{print_values(share)}
};
\end{axis}
\end{tikzpicture}

\end{document}

念のため何点か補足しておきます。まずLuaLaTeX文書内にLuaコードを書く場合、直接\directluaプリミティブを使用すると文字のエスケープが厄介です6luacodeパッケージが定義するluacode*環境を用いると、自然なLuaコードを書けるので、利用しました。第二にkpseライブラリはtexluaで利用する際にはkpse.set_program_name()による初期化が必要ですが、LuaTeX文書から呼び出す場合は不要のため省略しています。第三にprint_*()という名前の関数をいくつか定義して、calc_share()関数がテーブルの形で返す集計結果をpgfplotsの入力と各種設定に使える形式の文字列に変換して、最終的にtex.sprint()でTeX処理系側に渡しています。最後に、出力結果のグラフで各種TeX処理系のロゴを使うために、ちょっとだけアレなTeX on LaTeXをやっていますが、見なかったことにしてください。

さて、上記のLuaLaTeX文書を処理すると、目的のグラフが得られます。TeX Liveドキュメントツリーに含まれるほとんどすべてのPDF文書を走査するので、実行にはさすがに少し時間がかかります。手許での処理時間は約30秒でした。

というわけで、手許の最新版TeX Live 2019についての、お待ちかねの集計結果は次の通りです。

【TeX Live 2019収録PDFドキュメントにおけるPDF生成プログラムのシェア】

TeX Liveに含まれるドキュメントのPDF生成プログラムシェア

一見してpdfTeXが圧倒的多数派です。続いて多かったのはGhostscript (dvips) 経由で生成されたPDFでした。LuaTeXは第3位で、シェアを計算すると 9.7% という結果になりました。個人的には、予想よりちょっと低いシェアだったかなという気がします。日本人にとっては未だに馴染みの深いdvipdfmx経由のPDF生成フローは世界的にはかなりマイナーだということもわかり面白い結果です。

しかし、TeX Liveドキュメント全体の中でLuaTeX製PDFが占める割合をもって「現在のLuaTeXのシェア」と言うのは無理があります。TeX Liveにはかなり古いパッケージも多く含まれており、長いことメンテナンスされていない、いわゆる“枯れた”パッケージも少なくありません。そうした古いパッケージの、大昔に生成されたPDFはLuaTeXで生成されているはずもなく、歴史の長いTeX処理系ほど有利になってしまうという不公平が生じています。

ちょうどPDFの文書プロパティにはPDFの生成日時の情報が“CreationDate”項目に格納されているので、これを用いて直近5年に生成されたPDFに絞って再度集計を行ってみましょう。そのためにcalc_share()関数を少し改変して、次のようにcalc_share2()関数を定義します。

function calc_share2(doc_dir, start, stop)
  local share = {
    ['LuaTeX'] = 0,
    ['XeTeX'] = 0,
    ['pdfTeX'] = 0,
    ['Ghostscript'] = 0,
    ['dvipdfmx'] = 0,
    ['Others'] = 0
  }

  for _, fn in ipairs(list_pdf(doc_dir)) do
    local doc = pdfe.open(fn)
    if pdfe.getstatus(doc) ~= 0 then
      warn('Cannot open File %s', fn)
    else
      local info = pdfe.getinfo(doc)
      if info == nil then
        warn('No info in File %s', fn)
      else
        local info_producer = pdfe.getstring(info, 'Producer')
        if type(info_producer) ~= 'string' then
          warn('No Producer entry in File %s', fn)
        else
          local info_creation = pdfe.getstring(info, 'CreationDate')
          if type(info_creation) ~= 'string' then
            warn('No CreationDate entry in File %s', fn)
          else
            local year = tonumber(info_creation:sub(3, 6))
            if type(year) ~= 'number' then
              warn('Failed to parse CreationDate entry in File %s', fn)
            else
              if start <= year and year <= stop then
                local producer = judge_producer(info_producer)
                share[producer] = share[producer] + 1
              end
            end
          end
        end
      end
    end
    pdfe.close(doc)
  end

  return share
end

share = calc_share2(doc_dir, 2015, 2019)

【直近5年 (2015-2019) に絞ったPDF生成プログラムのシェア】

直近5年間 (2015-2019) に絞ったシェア

首位は相変わらずpdfTeXでしたが、LuaTeXが2位に浮上していました。LuaTeXのシェアは約 14.4% です。当然といえば当然ですが、近年TeX系開発者の中でのLuaTeXの採用率は上がってきていると言うことはできそうです。

とはいえ、今回明らかになったLuaTeXのシェアは私が想像していたよりも小さかったというのが正直なところです。本稿の冒頭でも注意した通り、今回はTeX Liveに含まれるPDFドキュメントを対象に計測を行ったので、ほとんどのPDF作者がTeX系の開発者、つまりTeXを使いこなしているエキスパートと呼べる人々です。これは仮説に過ぎませんが、エキスパート間でのLuaTeXシェアがこの程度ということは、初心者を含む一般のTeXユーザにおけるLuaTeXのシェアはこれよりさらに小さな値になるのではないでしょうか。

もっと細かくどのドキュメントがどのPDF生成プログラムで制作されているのか見てみたいという方のために、今回グラフ化するにあたって取得した生データはgistに置いておきます。

今回作成したシェア計算を行うLuaLaTeX文書では、calc_share()関数に与えるディレクトリを変えるだけで、任意のディレクトリ配下のPDFファイルについてTeX処理系のシェアを計算することができます。生憎私の手許にはTeX Liveのドキュメントツリーよりも良いTeX製PDFファイル群の持ち合わせがありませんが、幸運にも多様な著者による多様なTeX組のPDFをお持ちの方がもしいらっしゃいましたら、ぜひLuaTeXのシェアを算出してみて欲しいと思います。

Happy LuaTeXing!


*「素人質問で恐縮なのですが、今回の知見は何の役に立つのでしょうか?」
W「えー、例えばTexdocもtexluaで動いていてpdfeライブラリが使えるはずなので、LuaTeXの選択圧を強めるために非LuaTeX製のPDFは表示しない、などということができますね」
*「いや、それはどうなのよw」
W「ちなみにTexdocのマニュアルもXeTeX製なんですが🙃」


  1. 詳細はAdobe社のヘルプページを参照してください。 ↩︎

  2. 具体的にはhyperref, hyperxmp, pdfxパッケージなどが利用できます。 ↩︎

  3. PDFの文書プロパティを表示できるビューアなら別にAcrobat Readerでなくても構いません。PDFを目でパースできる方ならテキストエディタでもOK。 ↩︎

  4. TeX Live 2019など、最近のTeX環境ではxdvipdfmxとdvipdfmxプログラムの実体は同じもののようです。ただ、起動モードが異なるため、それぞれxdvipdfmx, dvipdfmxのように振る舞います。今回はそうした細かいことは気にしないことにします。 ↩︎

  5. もしかすると文書プロパティ以外の部分で、各TeX処理系が生成するPDFに特徴的な箇所があるかもしれませんが、やはり今回はLuaTeXのシェアを調べることが目的なので、dvipdfmxによる変換の前がどうであったかまでは深追いしません。 ↩︎

  6. \directluaの引数が内蔵のLua処理系に渡される前に、まずTeXの字句解析が適用されてしまうためです(参考)。 ↩︎