マクロツイーター

はてダから移行した記事の表示が崩れてますが、そのうちに直せればいいのに(えっ)

実験レポート:文書情報が文字化けしない hyperref の設定(1)

hyperref パッケージを用いて PDF 文書を作成する時に頻出するトラブルとして、文書情報(「文書のプロパティ」中のタイトル等、あるいは「しおり」)の文字列の文字化けが挙げられる。正しい出力を得るためには、hyperref のオプションや周辺ツールの設定を正しく行う必要があり、その設定は用いる TeX エンジンにより異なる。(日本では)pTeX 系については比較的情報が集まっている*1が、それ以外のエンジンに関する情報が乏しいので纏めて調べてみたい。

hyperref では文書情報の PDF 文字列の出力形式を制御するオプションとして次のものが用意されている。

  • unicode: 「PDF 文字列を Unicode にする」と(だけ)説明されている。
  • CJKbookmarks: CJK パッケージのためのものらしい。
  • pdfencoding=<値>: ここで <値>pdfdocunicodeauto の何れか。

ところが何故か知らないが、これらのオプションが具体的に何をするものかの説明がほとんど見当たらない。pdfencoding に関してはマニュアルに掲載すらされていない。(ChangeLog で幾らか言及されてるが、そこにも「何なのか」の説明はない。)さらに、(周知のとおり)hyperref の実装コードは非常に巨大で複雑なので、コードを追って動作を理解するというのがかなり困難である。仕方がないので、不確実さは増すが、「実験」によって適切な設定を探り出すことにする。

  • 8 ビット欧文 TeXlatex、pdflatex)
  • LuaTeX(lualatex)
  • XeTeX(xlatex)
  • 8 ビット欧文 TeX + CJK パッケージ

pTeX 系は実験によって何かを探究する対象にはしないが、比較のために以下の環境について同じ実験を行ってその結果について既知の知識を補足する。

  • pTeX/upTeX (platex、uplatex)
  • pTeX/upTeX + pxjahyper パッケージ
実験内容
% 文字コードはUTF-8
\documentclass[a4paper]{article}
\usepackage[T1]{fontenc}
\errorcontextlines=9
\usepackage[(オプション)]{hyperref}
%\usepackage{CJK} %<CJK>
%\AtBeginDocument{\begin{CJK*}{UTF8}{gbsn}} %<CJK>
%\AtEndDocument{\end{CJK*}} %<CJK>
%\usepackage{pxjahyper} %<pxjahyper>
\begin{document}
\section{!A B-C}                                      %(1)
\section{\textexclamdown A B\textemdash \c{C}}        %(2)
\section{\textexclamdown A B\textemdash \c{C}\NG}     %(3)
\section{\textexclamdown あ B\textemdash \c{C}\NG}    %(4)
\end{document}

上の文書をコンパイルして、「しおり」付きの PDF 文書を作る。その上で以下のものを調べる。

  • test.out 中に記された「節タイトルの PDF 文字列に相当する」テキストデータ
  • Adobe Reader で閲覧した時に実際に表示される節タイトルの文字列

詳細は以下の通り。

  • 試験対象ごとのコンパイル方法は以下の通り。
    • 8 ビット欧文 TeX: pdflatex(2回)で変換。
    • LuaTeX: lualatex(2回)で変換。
    • XeTeX: xelatex(2回)で変換。
    • 8 ビット欧文 TeX + CJK パッケージ: <CJK> の行を有効にして、pdflatex(2回)で変換。
    • pTeXplatex(2回)→ dvipdfmx で変換。
    • pTeX + pxjahyper パッケージ: <pxjahyper> の行を有効にして、platex(2回)→ dvipdfmx で変換。
    • upTeX + pxjahyper パッケージ: <pxjahyper> の行を有効にして、uplatex(2回)→ dvipdfmx で変換。
  • (オプション) については次のものを試す。なお、DVI 出力のエンジンを用いる場合は併せて dvipdfmx オプションを指定する。
    • (無し)
    • unicode
    • pdfencoding=pdfdoc
    • pdfencoding=unicode
    • pdfencoding=auto
  • (1)〜(4) の各節タイトルのテキストは以下のような性質をもつ。
    • (1) は ASCII のみからなる。
    • (2) は ASCII 外の文字を含むが PdfDocEncoding の範囲に収まる。
    • (3) は PdfDocEncoding にない文字を含む。(CJK 文字を含まない)
    • (4) は pTeX 系エンジンおよび CJK パッケージが「CJK 文字」として扱う文字を含む。
    \textexclamdown は〈¡〉(U+00A1)、\textemdash は〈—〉(EM DASH;U+2014)、\c{C} は〈&#c7;〉(U+00C7)、\NG は〈Ŋ〉(U+014A)を表す。〈あ〉の符号値は U+3042 である。)
  • XeTeX 及び LuaTeX 上の CJK を扱うパッケージ(xeCJK、LuaTeX-ja 等)を用いた場合については、PDF 文字列においては、CJK 文字もそれ以外の文字も同じ扱いになる(パッケージ不使用の場合と同じになる)はずである。*2
  • 上の文書では(欧文の)特殊文字を文字命令(LICR)で入力している。inputenc パッケージを用いて非 ASCII 文字を直接入力した場合も結局は文字命令に展開される(例えば〈¡〉は \textexclamdown に一度展開される)ので同じ結果のはずである。
  • 8 ビット欧文 TeX(CJK パッケージなし)の場合は、そもそも UTF-8 の直接入力が無効なので、(4) のような非 ASCII 文字を含むテキスト入力は不適切である。少なくとも〈あ〉の文字を入力したとは見做されない。*3
前提知識:PDF文字列のエンコーディング

PDF の仕様では、PDF 文字列(本来はバイト列)を「文字列」として解釈する場合は以下のようなエンコーディングを仮定している。

  • 先頭 2 バイトが〈FE FF〉(UTF-16BE の BOM に相当する)である場合は、UTF-16BE を仮定する。(BOM を除いたのが文字列となる。)
    • <FEFF00500440046658766F8> → 「PDF文書」
    • (\376\377\003\265\000-\000T\000e\000X) → 「ε-TeX
  • それ以外は、PdfDocEncoding という特定のエンコーディングを仮定する。
    • <41646f6280> → 「Adob€」
    • (\050u\051pTeX) → 「(u)pTeX
  • pdfencoding キーの値 unicode は UTF-16BE、pdfdoc は PdfDocEncoding を指していることが予想される。

hyperref が out ファイルに書き出す「結果のテキスト」は通常の(16 進でない)文字列リテラル( ) 内の文字列である。原則としてこれは PDF ファイルにそのまま書き出されて文字列リテラルとして解釈されるので、上記の何れかの符号化に従うものでなければならない。ただし、環境によっては、PDF に変換する際に「結果のテキスト」の加工が行われることがある(pdf:tounicode special 使用時など)。

結果:全般

実験した全ての環境において以下が成立した。

  • unicode オプションと pdfencoding=unicode は等価である。((さらに、hyperref のオプションに unicodepdfencoding=pdfdoc を両方指定すると、後で指定したものが有効になる。ここから、この 2 つが同系統のオプションであることが推定できる。))
  • オプション無しの時の動作は pdfencoding=pdfdoc と等価である。(つまりキー pdfencoding の既定は pdfdoc である。)

従って、以降では各環境において pdfencoding の値を pdfdocunicodeauto の各々にした場合の結果を列挙する。結果は次の書式で示すことにする。

先頭の番号 (2) は test.tex 中の行 (2) に対応する結果であることを表す。続く ( ) の中にあるのが test.out に出力されたバイト列である。この列がある文字列をある符号化で表したものに等しい場合は、その文字列を記し、後ろに符号化の名前を補足した。(ASCII の場合は省略した。)*4下の行の 「 」 内は Adobe Reader で閲覧したときに実際にしおりに表示された文字列を示している。これは上のリテラルを PdfDocEncoding(pdfdoc)か UTF-16BE(unicode)の何れかで解釈したものに等しいはずであり、何れであるかを後ろに補足した。

8 ビット欧文 TeX/pdfencoding=pdfdoc
  • (1) (!A B-C)
    → 「!A B-C」[pdfdoc]
  • (2) (\241A B\204\307)
    → 「¡A B—Ç」[pdfdoc]
  • (3) (\241A B\204\307)
    → 「¡A B—Ç」[pdfdoc]
  • (4) (\241あ B\204\307) [UTF-8]
    → 「¡Aㆇ B—Ç」[pdfdoc]

pdfencoding=pdfdoc とすると PdfDocEncoding のリテラルが出力された。(3)(4) では PdfDocEncoding にない〈Ŋ〉が欠落している。(コンパイル時にその旨の警告が出ている。)前述の通り (4) での〈あ〉の入力は不正であるが、その結果は〈A2 81 82〉というバイト列を PdfDocEncoding で解釈したものになった。(何れにしても (4) の入力は無意味である。)

8 ビット欧文 TeX/pdfencoding=unicode
  • (1) (\376\377\000!\000A\000\040\000B\000-\000C)
    → 「!A B-C」[unicode]
  • (2) (\376\377\000\241\000A\000\040\000B\040\024\000\307)
    → 「¡A B—Ç」[unicode]
  • (3) (\376\377\000\241\000A\000\040\000B\040\024\000\307\001\112)
    → 「¡A B—ÇŊ」[unicode]
  • (4) (\376\377\000\241\000?\000?\000?\000\040\000B\040\024\000\307\001\112)
    → 「¡Aã B—ÇŊ」[unicode]

pdfencoding=unicode とすると UTF-16BE のリテラルが出力された。今度は〈Ŋ〉の文字も正しく出力されている。((なお、(4) の「結果」の中の 3 つの ? はそれぞれ A2、81、82 のバイトである。\000?... の部分の 6 バイトは〈00A2 0081 0082〉という 3 文字に解釈されるはずだが、U+0081 と U+0082 は制御コードなので画面上では U+00A2〈¢〉だけが見えている。繰り返すが、(4) の入力は無意味である。))

8 ビット欧文 TeX/pdfencoding=auto
  • (1) (!A B-C)
    → 「!A B-C」[pdfdoc]
  • (2) (\241A B\204\307)
    → 「¡A B—Ç」[pdfdoc]
  • (3) (\376\377\000\241\000A\000\040\000B\040\024\000\307\001\112)
    → 「¡A B—ÇŊ」[unicode]
  • (4) (\376\377\000\241\000?\000?\000?\000\040\000B\040\024\000\307\001\112)
    → 「¡Aã B—ÇŊ」[unicode]

(1) と (2) は pdfencoding=pdfdoc、(3) と (4) は pdfencoding=unicode と同じ結果になった。つまり、pdfencoding=auto は「PdfDocEncoding で表せる場合はそれで、それ以外は UTF-16BE で表す」ことを意味していると推定される。

LuaTeX/pdfencoding=pdfdoc

「8 ビット欧文 TeX/pdfencoding=pdfdoc」と全く同じ結果になった。注意すべきなのは、UTF-8 入力が前提の LuaTeX の場合は (4) の行も正当な入力(つまり〈あ〉を含む文字列)であるということである。従って、LuaTeX の場合は「(4) は正しく処理できなかった」という結論になる。

LuaTeX/pdfencoding=unicode

(4) 以外は「8 ビット欧文 TeX/pdfencoding=pdfdoc」と全く同じ結果になった。

  • (4) (\376\377\000\241\060\102\000\040\000B\040\024\000\307\001\112)
    → 「¡あ B—ÇŊ」[unicode]

こちらは〈あ〉の文字も正しく処理されている。((\060\102 が〈あ〉(U+3042)の UTF-16BE 表現になっている。))

LuaTeX/pdfencoding=auto

(4) 以外は「8 ビット欧文 TeX/pdfencoding=auto」と全く同じ結果。(4) は「LuaTeX/pdfencoding=unicode」と同じ結果。すなわち、これも全ての入力が正しく処理されている。

XeTeX/pdfencoding=pdfdoc
  • (1) (!A\040B-C)
    → 「!A B-C」 [??]
  • (2) (¡A B—Ç) [UTF-8]
    → 「¡A B—Ç」 [??]
  • (3) (¡A B—ÇŊ) [UTF-8]
    → 「¡A B—ÇŊ」 [??]
  • (4) (¡あ B—ÇŊ) [UTF-8]
    → 「¡あ B—ÇŊ」 [??]

この場合、全ての入力が正しく処理された。しかし .out の出力を見ると、不合理なことに、UTF-8 の文字列そのまま出力されて、これだと本来は Reader で閲覧したときの文字列は化けるはずである。それが正しく表示できるということは、xdvipdfmx がリテラル文字列について何らかの加工を行ったことが判る。これはちょうど dvipdfmx で UTF8-UCS2 の ToUnicode マップを指定した時の動作と等価であるが、xdvipdfmx は特に UTF8-UCS2 の CMap ファイルは使用していないようである。恐らく、xdvipdfmx では「UTF-8 → UTF-16BE の変換を行う」という仕様が規定されていて、なおかつ、hyperref もそれを前提として UTF-8 の文字列をそのまま出力しているのだと思われる。

XeTeX/pdfencoding=unicode

「LuaTeX/pdfencoding=unicode」と同じ結果。つまり、リテラル文字列が(先頭の BOM から)元々 UTF-16BE であると判断できる場合は、「変換」を行わず、正しい表示が得られている。*5

XeTeX/pdfencoding=auto

「XeTeX/pdfencoding=pdfdoc」と同じ結果。((pdfencoding=unicode の場合も正しい表示になるが警告が出る。だから hyperref では pdfencoding=pdfdoc の結果が想定される入力と判断したのかも知れない。))

続く

*1:(u)pLaTeX + dvipdfmx の場合は要するに pxjahyper パッケージを読み込めばそれで一件落着。

*2:このことはこれらのパッケージの処理過程から推定できる。

*3:〈A3 81 82〉というバイト列が「直接出力」される状態になっている。

*4:つまりこの例の場合、test.out に出力されたバイト列は〈66 6F 6F A2 81 82〉である。

*5:ただし、この際に「UTF-16 への変換が失敗した」という旨の警告が出ているので、もしかしたら、これは「想定外」の動作なのかも知れない。