この記事は SATySFi Advent Calendar 21 日目の記事です。
もともと個人的なメモとしてまとめていた、SATySFi のページ分割に関するアイデアを公開します。あくまでも一ユーザの意見なのでツメが甘いところが多々あるかと思いますが、その際はご指摘いただけると幸いです。また、具象構文は SATySFi 0.1.0 のものを採用しています。
SATySFi を使ったことがあり、block-boxes
等の概念を理解している人
現在 SATySFi でページ分割をする際には、page-break
や page-break-multi-column
といったプリミティブを利用します。これらのプリミティブは大まかに言うと、block-boxes
を入力として、それを適切な長さに切り分けページに配置した document
を生成するというものです。この際ページ上の配置位置やヘッダ・フッタはこれらのプリミティブの引数として設定します。
基本的な組版はこの仕組みで十分なのですが、少し特殊なことをしようとすると不足を感じることがあります。例えば、
- ページ全体を利用した画像配置(LaTeX の画像配置の p オプション)
- 多段組で、段の先頭に画像を配置する
- 文書中の任意の位置から多段組を開始する(LaTeX の multicol のような機能)
といったことは現状の API では実現が難しいと思われます。そこで本稿では、ページ分割を一般化することでこうした組版を実現する手法を模索します。
基本的なアイデアは、page-break
の機能を以下の 2 つのプリミティブに分離するというものです(名前は適当です)。
cut-block
:block-boxes
を指定した長さで切り出すgenerate-document
: 切り出したblock-boxes
をページに配置する(document
を生成する)
cut-block
は以下のようなシグネチャを持ちます。
% length -> block-boxes -> (block-boxes, block-boxes)
cut-block l bb
第一引数 l
は切り出す長さ、第二引数 bb
は切り出し対象の block-boxes
です。返り値として、切り出された block-boxes
と残りの block-boxes
がタプルとして返却されます。
当然指定された長さ丁度で切り分けられるとは限らないので、より正確には「 l
を超えない範囲で最大の長さを切り出す」ことになります。また、ここでの分割は現在の page-break
同様 add-footnote
によって追加した block-boxes
が含まれ得るとします。
一方 generate-document
は以下のようなシグネチャを持ちます。
% page-func -> list block-boxes -> document
generate-document pagef bblst
第一引数の pagef
は各ページでコンテンツをどの座標に配置するかを決定し、第二引数の block-boxes
のリストが実際に配置されるコンテンツです。リストの一要素が 1 ページに対応します。返り値として作成した document
が返却されます。また、この呼出しの中で hook-page-break
等で指定したフックが実行されます。
通常のページ分割は上記の 2 つのプリミティブの組み合わせで実現できます。
val page-break pagef bb =
let rec inner bb =
if is-block-nil bb then
[]
else
let (head, rest) = cut-block text-height bb in
head :: (inner rest)
in
let bblst = inner bb in
generate-document pagef bblst
ヘッダやフッタを挿入する場合には以下のように実装できます。
val page-break ctx pagef bb =
let rec inner n bb =
if is-block-nil bb then
[]
else
% ヘッダの生成
let header-bb = gen-header ctx n in
let header-length = get-natural-length header-bb in
% フッタの生成
let footer-bb = gen-footer ctx n in
let footer-length = get-natural-length footer-bb in
% 主コンテンツの切り出し
let effective-height = text-height -' (header-length +' footer-length) in
let (main-bb, rest) = cut-block effective-height bb in
% ヘッダ・フッタとの結合
let page = header-bb +++ main-bb +++ footer-bb in
% 結合
page :: (inner (n + 1) rest)
in
let bblst = inner 1 bb in
generate-document pagef bblst
フッタが必ずページ最下部に配置されるようにするために、main-bb
と footer-bb
の間に block-skip
を入れても良いでしょう。重要なのは、ヘッダ・フッタの概念が SATySFi 処理系から離れクラスファイルで導入できるものとしているおかげで、こうした細かな調整が記述できるようになっているということです。
また実装は省略しますが、相互参照と組み合わせることでページ全体に渡る画像表示(LaTeX の figure の p オプション)も実現できるはずです。
現状の page-break-two-column
相当の関数は以下のように実装できます。
val page-break-two-column' ctx pagef bb =
let rec inner n bb =
if is-block-nil bb then
[]
else
% ヘッダの生成
let header-bb = gen-header ctx n in
let header-length = get-natural-length header-bb in
% フッタの生成
let footer-bb = gen-footer ctx n in
let footer-length = get-natural-length footer-bb in
% 主コンテンツの切り出し
let effective-height = text-height -' (header-length +' footer-length) in
let (main-left, rest) = cut-block effective-height bb in
let (main-right, rest) = cut-block effective-height rest in
let main-bb = side-by-side main-left main-right in
% ヘッダ・フッタとの結合
let page = header-bb +++ main-bb +++ footer-bb in
% 結合
page :: (inner (n + 1) rest)
in
let bblst = inner 1 bb in
generate-document pagef bblst
引数の bb
には予めテキスト幅半分で組まれたものが渡されると仮定しています。また page-break-multi-column
も同様の方法で実装できるでしょう。
ここで side-by-side
は block-boxes
を水平方向に連結するもので、現状の SATySFi であれば embed-block-top
などを用いて実装できるかと思います(ベースラインを合わせるのはここではあまり望ましくないですが)。
さらに、ヘッダ・フッタの生成に相当することを各カラム単位で行えば、段の先頭に画像を配置することもできると思われます。