本稿は,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.
これによって,グループ内だけでマクロの中身を追加することができます.(脱線終了)

