本稿は,LaTeX2e 2020/02/02におけるNFSS2の更新に関して,ltnews31.pdfで「Font series defaults per document family」と題されている節に対して,何が問題なのかをぐだぐだ書いた前稿の続き,mweights.styによる解決編です.
(注意):LaTeX2e 2020/02/02がリリースされ,そこではmweights.sty相当の機能が組み込まれています.さらに\usepackage{mweights.sty}
を明示したとしても実際にはmweights.styは読み込まれません.したがって,mweights.styを前提としたコードは正しく動かない可能性がありますので注意してください.
ただし,LaTeX2e 2020/02/02はmdweights.styのアイデアを使ってはいますが,mdweights.styそのままではありません.
なお,mdweights.styは2020/1/23付けでReadmeが更新されて,新しいLaTeX2eではmdweights.styは不要である旨が追記されています.
問題と解決案
前稿で述べた問題は結局のところ以下のものです.
新しいフォントを導入する際に,不用意に
\mddefault
や\bfdefault
を変えてしまうと他のフォントにも影響が出る.かといって変更しないと汎用性が低くなってしまうケースが出てくる.
これへの対処は,明解・簡潔な見事なアイデアによってmweight.styで提示されています.そのアイデアは,まさにltnews31.pdfの節題
Font series defaults per document family,フォントのfamilyごとにseriesのデフォルトを作ろうよ
です.これは全然気が付かなった観点でした.現象は認識してましたが,思いっきり対症療法してました.
mweights.styの提案する解法を,前稿のfuturaのケースのケースに適用すると次のようになります.
\renewcommand{\sfdefault}{bfu}%%% sfをFuturaにする
%\renewcommand{\bfdefault}{b} %boldfaceをFutura-Boldに
%\renewcommand{\mddefault}{md} %mediumをFutura-Mediumに
\renewcommand{\bfseries@sf}{b} %boldfaceをFutura-Boldに
\renewcommand{\mdseries@sf}{md} %mediumをFutura-Mediumに
\RequirePackage{mdweights}
ここでは,SansSerifだけなので,\bfseries@sf
と\mdseries@sf
の二つですが,三つのfamily,rm,sf,ttすべての場合は\(bf|md)series@(rm|sf|tt)
の六つのマクロでrm/sf/ttのそれぞれの\mdseries
と\bfseries
のデフォルト値を定めます.そして,mdweights.styは\(rm|sf|tt)famliy
,\(md|bf)series
がこれらの新しいデフォルト値を適切に扱うようにします.つまり,上のFuturaBT.styでは
-
\rmfamily
の\bfseries
はbx(LaTeX2eの\bfdefault
の値) -
\sffamily
の\bfseried
はb(\bfseries@sf
の値) -
\ttfamily
の\bfseries
はbx(LaTeX2eの\bfdefault
の値) -
\rmfamily
の\mdseries
はm(LaTeX2eの\mfdefault
の値) -
\sffamily
の\mdseries
はmd(\mdseries@sf
の値) -
\ttfamily
の\mdseries
はm(LaTeX2eの\mddefault
の値)
という定義が実現されます.ここでは\sffamily
しか書き換えていませんが,mweights.styによって,familyごとにseriesのデフォルト(従来の\mddefault
と\bfdefault
に相当)が定められます.
おかしなサンプル
何かの状態や動作を見たり,理解するには変なケースをサンプルにするといいので,おかしなサンプルをあげてみます.
\documentclass{article}
\def\rmdefault{ptm}
\def\sfdefault{phv}
\def\ttdefault{pcr}
\DeclareFontFamily{OT1}{ptm}{}
\DeclareFontShape{OT1}{ptm}{m}{n}{<-> ptmr7t}{}
\DeclareFontShape{OT1}{ptm}{big1.5}{n}{<->s*[1.5] ptmr7t}{}
\DeclareFontShape{OT1}{ptm}{big2}{n}{<->s*[2] ptmr7t}{}
\DeclareFontFamily{OT1}{phv}{}
\DeclareFontShape{OT1}{phv}{m}{n}{<-> phvr7t}{}
\DeclareFontShape{OT1}{phv}{big.5}{n}{<-> s*[.5] phvr7t}{}
\DeclareFontShape{OT1}{phv}{big2.5}{n}{<-> s*[2.5] phvr7t}{}
\DeclareFontFamily{OT1}{pcr}{}
\DeclareFontShape{OT1}{pcr}{m}{n}{<-> pcrr7t}{}
\DeclareFontShape{OT1}{pcr}{big.25}{n}{<-> s*[.25]pcrr7t}{}
\DeclareFontShape{OT1}{pcr}{big3}{n}{<-> s*[3] pcrr7t}{}
\makeatletter
\def\mdseries@rm{m}
\def\mdseries@sf{big.5}
\def\mdseries@tt{big3}
\def\bfseries@rm{big1.5}
\def\bfseries@sf{big2.5}
\def\bfseries@tt{big.25}
\makeatother
\usepackage{mweights}
\begin{document}
AAAAA\textsf{BBBB}\texttt{CCCC}
\par
\textbf{AAAAA\textsf{BBBB}\texttt{CCCC}}
\end{document}
コンパイルして結果をみると正しい挙動ですが,おかしなものが出来上がります.
- 1行目「AAAA」:
\rmfamily\mdseries
:10ptのTimes-Roman - 1行目「BBBB」:
\sffamily\mdseirs
:5ptのHelvetica - 1行目「CCCC」:
\ttfamily\mdseris
:30ptのCourier - 2行目「AAAA」:
\rmfamily\bfseries
:15ptのTimes-Roman - 2行目「BBBB」:
\sffamily\bfseries
:25ptのHelvetica - 2行目「CCCC」:
\ttfamily\bfseries
:2.5ptのCourier
\mdseries
と\bfseries
のデフォルトがfamilyごとに変わっていることがサイズの違いで現れるようにしています.一つ一つ追いかけてみれば動作を確認できます.
mweights.styの実装
mweights.styの実装そのものはかなりシンプルです.まず,familyごとのデフォルト値を定める\bfseries@sf
などを適切に処置するようにして,その上でフォントの属性の値の変更先が,変更命令が使用されたときのfamilyに対応付けられた値になるように\rmfamily
や\bfseries
そのものを書き直します.
ただし,\rmfamily
や\bfseries
といった基本的なマクロを書き換えるので,これらのマクロに何らかの動作を追加しているようなPackageやclassファイルとは相性がよくないでしょう.
mdweights.styの中を見る
実際にmweights.styの中身を見てみましょう.mweights.styの一番最初です.
###familyとseriesのデフォルトを決める\mweights@init
\def\mweights@init{%
% Define any undefined \mdseries@rm etc.
% Defined \mdseries@rm etc. assumed to be fully expanded!
\ifdefined\mdseries@rm\else\edef\mdseries@rm{\mddefault}\fi
\ifdefined\bfseries@rm\else\edef\bfseries@rm{\bfdefault}\fi
\ifdefined\mdseries@sf\else\edef\mdseries@sf{\mddefault}\fi
\ifdefined\bfseries@sf\else\edef\bfseries@sf{\bfdefault}\fi
\ifdefined\mdseries@tt\else\edef\mdseries@tt{\mddefault}\fi
\ifdefined\bfseries@tt\else\edef\bfseries@tt{\bfdefault}\fi
% In case any unexpanded macros present in \rmdefault, etc
\edef\rmdef@ult{\rmdefault}%
\edef\sfdef@ult{\sfdefault}%
\edef\ttdef@ult{\ttdefault}%
\edef\bfdef@ult{\bfdefault}%
\edef\mddef@ult{\mddefault}%
\edef\famdef@ult{\familydefault}%
}
まず,初期化を担う\mweights@init
が定義されています.\ifdefined
はeTeX拡張のプリミティブで,次に置かれるトークンが定義済なら真,未定義なら偽となる「\if
」です1.
まず,\mdseries@rm
などの三つの各familyにある二つのseriesの六つのデフォルトが定義されていなければ,それを対応する,LaTeX2eのデフォルトとして定義します.\edef
で定義しているので\mweights@init
が展開されるその場所での値が確定します.そのあと,やはりその時点での\rmdefault
などの内容を\rmdef@ult
などの対応するものに\edef
で保存します.コード内のコメントにもあるように,\edef
による定義なので定義されたものは一回で完全展開されるものになります.その場のfamilyなどに応じて動作を変えるマクロを作るために,容易に比較ができるようにという意図でしょうね.
このようにシステム(LaTeX2e)側のパラメータなどを別名で保存して,保存したものの方を自前のもので使うというのはよく使われる手法で,不用意に大事なものを壊さないためのガードにもなりますし,このように一段階くるんでおくと,一般的には細工を忍び込ませることもできるようになります.
実際の文書で最初に行われる初期化
\mweights@init
ですが実際に最初に使われるのは,mweights.styの最後の\AtBeginDocument
による部分です.インデントや改行はkbhondaが変えています.
% override default family with new \familydefault
\AtBeginDocument{%
\mweights@init
\ifx\famdef@ult\rmdef@ult
\rmfamily
\else\ifx\famdef@ult\sfdef@ult
\sffamily
\else\ifx\famdef@ult\ttdef@ult
\ttfamily
\fi\fi\fi}
\AtBeginDocument
はその引数の内容を@begindocumenthook
というマクロの「中身」の最後に追加します,そして,\@begindocumenthook
は\begin{document}
の処理の比較的最後の方で実行されます.この追加を実際に担うのは\AtBeginDocument
の下請けである\g@addto@macro
というマクロですが,このマクロの実装は面白いので後述します.
前回,フォントの属性の初期化は\precess@table
というマクロによって\begin{document}
の中で行われることを書きましたが,\@begindocumenthook
はそれよりも後で処理されます.そして,mweights.styでは,上述のように\AtBeginDocumentによって,
@begindocumenthook`に以下の処理を追加します.
-
\mweights@init
による初期化(\(md|bf)seris@(rm|sf|tt)
の定義と\(rm|sf|tt|md|bf|fam)def@ult
の定義) - \famdef@ultが\rmdef@ultならば\rmfamilyの実行
- \famdef@ultが\sfdef@ultならば\ttfamilyの実行
- \famdef@ultが\ttdef@ultならば\ttfamilyの実行
最初に初期化\mweights@init
をいれることで,mweights.styが要求する\bfseries@sf
のようなfamilyごとのデフォルトが定義されていないパッケージ(times.styなど)が\rmdefault
,\sfdefault
,\ttdefault
のどれかでも変更してしまっている場合でも,mweights.styが使われていないときと同じ挙動をすることが保証されます.
ただし,ここでの\(rm|sf|tt)family
はmweights.styで再定義されたものです.後述しますが再定義されたこれらのfamily変更命令は,LaTeX2eのカーネルのものがfamilyの変更のみを行うのとはちがい,familyの変更のほかにseriesも変更します.
先ほどのサンプルFuturaBT.styを使ってみます
\renewcommand{\sfdefault}{bfu}%%% sfをFuturaにする
\renewcommand{\bfseries@sf}{b} %boldfaceをFutura-Boldに
\renewcommand{\mdseries@sf}{md} %mediumをFutura-Mediumに
\def\familydefault{\sfdefault}%%%追記してみました
\RequirePackage{mdweights}
これで定義されるFuturaがデフォルトの欧文となるように,\familydefault
を変えてみました.この結果,\AtBeginDocument
に追加されたmweights.styの処理では,\familydef@ult
と\sfdef@ult
が一致して,mweights.sty版の\sffamiy
が実行されます.
話を簡単にするために,他のフォント変更パッケージが一切読み込まれていないとします(LaTeXのデフォルトのOT1/cmr/m/n/10が\process@tabel
で使われている状態とします).
もしもLaTeX2eのオリジナルの\sffamily
であったなら,seriesは変更されずmのままです.しかし,FuturaBT.styではデフォルトのseriesはmdなので整合がとれません.
一方,mweights.sty版では,この状況下では\sffamily
はfamilyを\sfdef@ult
,seriesを\mdseries@sf
に変更します.したがって,familyがbfu,seriesがmdとなり,意図通りのものが選ばれます.
結局のところ,\AtbeginDocument
で挿入される処理は,\process@tabel
が行うフォントの初期化に相当する処理のmweights.sty版を\begin{document}
内で行うようにするものです.なお,shapeやsizeにはmweights.styは関与しないでの\process@tabel
での結果がそのまま引き継がれます.
familyの変更
\rmfamily
の定義を例とします.\sffamily
,\ttfamily
もほぼ同様ですので省略します.比較のためにLaTeX2eのもともとの定義も列挙しています.みてわかるように長いですが,ここまでくれば処理内容自体はシンプルです.
%LaTeX2eのもともとの定義
\DeclareRobustCommand\rmfamily
{\not@math@alphabet\rmfamily\mathrm
\fontfamily\rmdefault\selectfont}
%%mweights.styの定義
\DeclareRobustCommand\rmfamily{%
\mweights@init
\not@math@alphabet\rmfamily\mathrm
% change the current series before changing the family
\ifx\f@family\sfdef@ult
\ifx\f@series\mdseries@sf\fontseries\mdseries@rm
\else\ifx\f@series\bfseries@sf\fontseries\bfseries@rm
\else\ifx\f@series\mddef@ult\fontseries\mdseries@rm
\else\ifx\f@series\bfdef@ult\fontseries\bfseries@rm
\else\fontseries\mdseries@rm
\fi\fi\fi\fi
\else\ifx\f@family\ttdef@ult
\ifx\f@series\mdseries@tt\fontseries\mdseries@rm
\else\ifx\f@series\bfseries@tt\fontseries\bfseries@rm
\else\ifx\f@series\mddef@ult\fontseries\mdseries@rm
\else\ifx\f@series\bfdef@ult\fontseries\bfseries@rm
\else\fontseries\mdseries@rm
\fi\fi\fi\fi
\else\ifx\f@family\rmdef@ult
\ifx\f@series\mdseries@rm\fontseries\mdseries@rm
\else\ifx\f@series\bfseries@rm\fontseries\bfseries@rm
\else\ifx\f@series\mddef@ult\fontseries\mdseries@rm
\else\ifx\f@series\bfdef@ult\fontseries\bfseries@rm
\else\fontseries\mdseries@rm
\fi\fi\fi\fi
\else\fontseries\mdseries@rm
\fi\fi\fi\fontfamily\rmdefault\selectfont}
共通の処理\not@math@alphabet\rmfamily\mathrm
は数式内では絵エラーを出す処理です.\rmfamiy
と\mathrm
はエラーメッセージに使われるだけですので,割愛します.
mweights.sty版では初期化\mweights@init
が行われたあと,フォント変更前の状態に関しての条件分岐がひたすら行われています.\f@family
,\f@series`は現状の(フォント変更前の)familyとseriesです.
-
familyが
\sfdef@ult
ならば- seriesが
\mdseries@sf
ならば\f@series
を\mdseries@rm
に - seriesが
\bfseries@sf
ならば\f@series
を\bfseries@rm
に - seriesが
\mddef@ult
ならば\f@series
を\mdseries@rm
に - seriesが
\bfdef@ult
ならば\f@series
を\bfseries@rm
に - seriesがそれ以外ならば\f@series
を
\mdseries@rm`に
- seriesが
-
familyが
\ttdef@ult
ならば- 同様の分岐なので略
-
familyが
\rmdef@ult
ならば- 同様の分岐なので略
-
familyがそれ以外ならば
\f@series
を\mdseries@rm
に
そのあと,familyを\rmdefault
に変えて(\f@family
を\rmdefault
にして),\selectfont
によって実際にフォントを切り替えます.
実は,この処理に疑問がないわけではないです.seriesが\mddef@ult
だったら,そのまま\mddef@ult
の方が自然な気もします(\mddef@ult
に相当するものが未定義の可能性もありますが).最後のfamilyの変更も\rmdafault
ではなく\rmdef@ult
の方がすっきりするような気もします.
ただ,\mweights@init
がかなり強いので,普通にフォントを切り替えている(\fontseries{\bfdef@ult}\selectfont
など低レベルの処理をしない)限りは\ifx\bfdef@ult
の分岐には入らなそうですし,この分岐に入ったとしても\bfdef@ult
の状態からfamilyを変えたら変更先のseriesは変更先の\bfseries@XX
であるべきだという仕様だと思うほうがよいのかもしれません.
そこで,ここでは以下のような指摘だけにとどめておきます.たとえば,\bfdef@ult
がbxで,\mdseries@rm
がbであって,bとbxでフォントが違うケースがありえます.この場合,\rmfamily
で書体が変わります.例えば,以下のファイルでPDFを作ります.
\documentclass{article}
\makeatletter
\DeclareFontShape{OT1}{cmr}{b}{n}
{<-> s*[2] cmb10}{}%%わかりやすいように大きくする
\def\bfseries@rm{b}
\makeatother
\usepackage{mweights}
\begin{document}
\makeatletter
\textsf{\bfseries AA{\rmfamily BB}CC}
\end{document}
\bfseries@rm
をbにしているので,AAはOT1/cmss/bx/n/10,BBはOT1/cmr/b/n/10(2倍にしています),CCはOT1/cmss/bx/n/10になります.BBBはOT1/cmr/bx/n/10ではありません.\rmfamily
でseriesもこのような変わる(\bfdef@ult
が,\rmfamily
で\bfseris@rm
になる)ことには注意が必要なケースがでてくるかもしれません.
この場合,\bfseries@sf
を明示的には定義はしていませんが,\mweights@init
で\bfseries@sf
はbxになっています.したがって,\bfdef@ult
の分岐には入らないのですが,\rmfamily
でseriesも変わることの例にはなります.
この意味で,mweights.styはいわゆる破壊的変更を行うことを留意しておく必要はあると思います2.
seriesの変更
最後にseriesの変更マクロです.\mdseries
はほぼ同様なので割愛します.
\DeclareRobustCommand\bfseries{%
\mweights@init
\not@math@alphabet\bfseries\mathbf
\ifx\f@family\rmdef@ult\fontseries\bfseries@rm
\else\ifx\f@family\sfdef@ult\fontseries\bfseries@sf
\else\ifx\f@family\ttdef@ult\fontseries\bfseries@tt
\else\fontseries\bfdefault\fi\fi\fi\selectfont}%
処理内容も素直なもので,seriesの値を各familyのデフォルトのbfseriesに値に変更するだけです.rm,sf,ttではないfamilyのときは従来のままの処理になっています.
おわりに
例によって長いですが,なんとかかんとか終わりです.実際のところ,LaTeX2e 2020/02/02ではmweights.styそのものは使われていないので,このような考察は不要といえば不要なのですが,何かの思いもよらないことがおきたときに,もしかすると役に立つかもしれません.
問題はこれに日本語が関係した場合にどうするかということです.texjp.orgのgithubのissue88で議論が進んでいますし,私が把握している限りここ以外に日本語で今回の更新の詳細が読めるのはここだけです(zrさん,aminophenさんに感謝 )2.
dev-jブランチに日本語のケースを含めた,aminophenさんの実装があるので,順番に頑張って読むことにします.
脱線:\g@addto@macro
\AtBeginDocument
ですが,その名前の通り,\begin{document}
の際に実行したいものを登録することができます.その定義は以下の通りです.定義の本体である\g@addto@macro
(global add to macro)も一緒に引用してあります.
\DeclareRobustCommand\AtBeginDocument{\g@addto@macro\@begindocumenthook}
\long\def\g@addto@macro#1#2{%
\begingroup
\toks@\expandafter{#1#2}%
\xdef#1{\the\toks@}%
\endgroup}
\g@addto@macro
というマクロはちょっと面白いです.\g@addto@macro\A{...}
という形で使うのですが,まず\A
がマクロとして定義されているのが前提です.未定義のときはエラーになります.\g@addto@macro\A{...}は
\Aを一回展開したものに「...」を後置したものを新たにマクロ
\A`としてグローバルに定義します.例示すると
\def\A{hoge\relax}
\g@addto@macro\A{foo}
\Aの中身は hoge\relax foo になります.
です.これは既存のマクロの中身に追加するのに便利で,LaTeX2eでは,何かのタイミングに処理をhook(引っかけるという意味の「フック」)させる際に引っかける処理を追加するために使われます.
実装に使われている手法は,グループの中でトークンレジスタ\toks@
に,もとのマクロと追加するものを代入するのですが,
\toks@\expandafter{#1#2}
なので,もとのマクロ(#1
の部分)が一回展開されます.そして追加の部分#2と連結されて\toks@
に入るという形です.\expandafter
で展開を行う典型的な手法です.それを改めて#1
で与えられたマクロに次のように定義しなおすのですが,
\xdef#1{\the\toks@}
\xdef
すなわち\global\edef
です.\gloabl
なのは今はグループの中だから外でも使えるようにするということでよいのですが,\edef
なので展開しきってしまいそうですが,中身が\the\toks@
です.じつは\the\toks@
のようなトークンレジスタの\edef
での展開は,完全に展開しきるのではなく,一回のみの展開という仕様になっています.
このために,(#1
の一回展開)と#2
が中身の#1
という名前のマクロが定義されます.つまり,#1
というマクロの中身に#2
が追加されるわけです.
なお,グローバルにすることでhookをかけるという意味では使いやすいものになりますが,追加そのものをローカルにすることも可能です.
\long\def\l@addto@macro#1#2{%
\begingroup
\toks@\expandafter{#1#2}%
\edef\x{\def\noexpand#1{\the\toks@}}% (1)
\expandafter\endgroup\x% (2)
}
(1)の\edef\x
の\edef
で\the\toks@
を一回展開し,\noexpand#1
でマクロ名に相当する#1
の展開を禁止します.これによって,\def#1{<#1の一回展開>#2}に展開される\x
というマクロが定義され,(2)で\expandafter
によって\endgroup
でグループが閉じる前に\x
が展開されグループの中がそとに引き出され,しかもそのあとにグループが閉じることになります2.
これによって,グループ内だけでマクロの中身を追加することができます.(脱線終了)