Tag Archives: XML

[XSL-FO試行錯誤]索引のページ番号表示で一部を太字にする

索引語に続くページ番号表示。「3,6,11-15,100,…」のように並んでいても、「どこがその語を説明している箇所なのか」は分かりません。そのようなときに、索引表現ではとくに重要・説明的である箇所のページ番号についてスタイルを変えて表現することがあります。これを実現するFOについて考えてみましょう。


「ある箇所について特別な処理をしたい」というとき、必要なのは次の情報です。

  • 「ある箇所」と他を区別するルール
  • 特別な処理を実現するFO

今回の場合、「通常の索引と、重要な1箇所の索引が区別できること」については元のXMLとXSLT処理の書き方によって
大きく異なると思われますので、それらを最終的に落とし込むFOについて検討します。

索引用FOの機能

索引用のFOの基本は、@index-keyによって指定されるキーと、そのキーを表示するための<fo:index-citation-reference>です。

索引 XSL-FOの基礎 第2版

これらのFOでは、該当するキーを含むページ番号について、FOプロセッサが列挙し表示を調整するという処理を行います。
つまり、FOプロセッサというユーザの手を離れた段階で処理されるので、「索引用のFOで一部のページ番号だけを太字にしたい」という要求を実現するには、このFOでは上手くいかなさそうだということを意味します。


索引用FOでは「2ページだけを太字にする」処理は難しい

索引にリンクを付けたい場合はfo:index-citation-list/@page-number-treatment="link"で表示されたページ番号にリンクが付与されます。

汎用のページ番号参照

あるページの参照だけであれば、fo:page-number-citaionに該当箇所のidを指定すれば実現できます。これを利用することにします。
制限として、数ページにわたるページ番号参照には対応できません。

汎用的なリンク用のFOであるfo:basic-linkでは、internal-destinationにidを指定することで内部リンクを付与できます。

よって、id情報があれば、該当の番号のページへジャンプするリンクが付与されたページ番号参照が実現できます。

最終的な方針とFO

重要な箇所の索引をspとします。通常の索引のときは、fo:wrapper/@index-keyを挿入する通常の索引用の属性を、spの索引のときはfo:wrapper/@idを本文に挿入します。このとき、spの前後でindex-keyの値は変えておきます。

本文のFOを通常の索引とspとで切り換える
<fo:block><fo:wrapper index-key="あんてなはうす1"/>アンテナハウスは、Data Usability Companyです。</fo:block>
<!-- spの索引語 -->
<fo:block ...><fo:wrapper id="antenna"/>アンテナハウスは、日本のソフトウェア企業で、XML自動組版ソフトウェアAntenna House Formatterや、PDFに関連した製品を開発・販売しています。</fo:block>
<fo:block ...><fo:wrapper index-key="あんてなはうす2"/></fo:block>
<fo:block ...><fo:wrapper index-key="あんてなはうす2"/></fo:block>

索引ページのFO
<fo:block ... space-after="2rem" font-size="72pt">索引</fo:block>
  <fo:block background-color="black" color="white">あ</fo:block>
  <fo:block text-align-last="justify" text-align="justify">アンテナハウス<fo:leader leader-pattern="dots" leader-alignment="center"/><!--
--><fo:index-page-citation-list merge-sequential-page-numbers="merge"
	merge-pages-across-index-key-references="merge" 
	merge-ranges-across-index-key-references="merge">
	<fo:index-page-citation-list-separator>,</fo:index-page-citation-list-separator>
	<fo:index-page-citation-range-separator>-</fo:index-page-citation-range-separator>
	<fo:index-key-reference ref-index-key="あんてなはうす1" page-number-treatment="link" />
</fo:index-page-citation-list><!--spの索引--><fo:inline>,<fo:inline font-weight="bolder"><fo:basic-link
 internal-destination="antenna"><fo:page-number-citation ref-id="antenna"/></fo:basic-link></fo:inline>,</fo:inline><fo:index-page-citation-list
  merge-sequential-page-numbers="merge"
  merge-pages-across-index-key-references="merge" 
  merge-ranges-across-index-key-references="merge">
	<fo:index-page-citation-list-separator>,
	<fo:index-page-citation-range-separator>-
	<fo:index-key-reference ref-index-key="あんてなはうす2"  page-number-treatment="link"/>
</fo:index-page-citation-list>
</fo:block>

fo:index-page-citation-listは複数のfo:index-page-referenceを持てますが、今回は特別扱いするfo:inlineを挟みたいため、一旦終了して特別処理の箇所の後に別のfo:index-page-citation-listを開始しています。

これをFormatterで組版し、冒頭に上げた画像の索引が作成できました。

補足

実作業で厄介なのはXSLTでの処理時に@index-keyを持たないspを、索引リスト作成時にどのようにして取得するかでしょうか。また、spである索引が登場する場合としない場合、sp前後に同じ語の索引が登場しない場合の「,」表示の分岐など、索引FOが吸収していた面倒な分岐も自前で用意する必要があるでしょう。

索引ウェビナーの宣伝

索引について、もう少し全般的な紹介をするウェビナーを2023年9月19,26日(火)16時に行います。今回の記事は応用実装よりでニッチ度が高いものでしたが、まだまだ話題がありますので、どうぞご検討ください。

XSL-FO 試行錯誤 SVG 2.0 inline-sizeのTips

2023年01月19日に『Antenna House Formatter V7.3』がリリースされました!
関連して、2023年01月31日(火)16:00-17:00 にZoomウェビナーとして『Antenna House Formatter V7.3 リリース!  新機能と利用シーン紹介』を開催します。


https://us06web.zoom.us/webinar/register/4716717860524/WN_ElB-ZrzFQzShzKGqWZMHqg

新機能紹介ということで、XSL-FOやCSSページ組版の基礎知識があることを前提としています。しかし、特に知識の無い方でも「自動組版でこんな細かいレイアウト調整が効くのか」と楽しめることを目指していますので、お時間が合いましたら是非参加登録いただければ幸いです。

ウェビナースライドはFormatter V7.3で組版しています。スライドはCSSで組んでいるのですが、話題がSVGについてなので、暫くぶりとなる「XSL-FO 試行錯誤」シリーズを題に含めました。
(前回のXSL-FO試行錯誤のカレンダー作成の続きについてはまた時間があるときにさせていただければ……)

さて、本記事ではウェビナーで割愛する部分について、先行して補足します。
ということで、見た目が地味なのであまり深く掘り下げない予定のinline-sizeによるテキストの折り返しについてです。

発表スライドの見出し部分にはSVG 2.0を使っています。

テキストを配置するとき、SVG 1.xでは折り返しを扱えませんでした。
よって、複数行のテキストを配置するときは次の手順(これ以外にもなくはないですが)です。

右揃えで3行でテキストを描画するとします。座標は適当です。

  1. 改行位置でテキストを分割し、別のtspanに分ける
  2. それぞれのtspanにx,yの座標を指定する。2行目であれば1行目から1文字分以上離れた位置にyを設定する
<!-- SVG 1.x -->
<text x="0" y="0">
<tspan text-anchor="end" x="250" y="0">Formatter V7.3</tspan>
<tpsan text-anchor="end"  x="250" y="10">リリース!</tspan>
<tspan text-anchor="end"  x="250" y="20">新機能と利用シーン</tspan>
<text>

text-anchor=”end”によって、xの位置がテキストの終端になっています。

x,yはSVG内の座標系なので、font-size:18ptのように絶対単位で指定されていると対応する高さを調整するのはちょっと手間ですね。
他にも拡大縮小の際などにこれを忘れずに変更しなければなりません。

メリット、といえるかは分かりませんが、あらかじめ分割しているので、意図しない行分割が発生するということはないでしょう。

SVG 2.0のテキスト折り返し機能を使えば、折り返しを自動化可能です。Formatter V7.3ではinline-sizeによる折り返しをサポートしています。
Scalable Vector Graphics (SVG) 2 11.4.1. The ‘inline-size’ property



<text x="0" y="0" inline-size="1000">Formatter V7.3リリース! 新機能と利用シーン</text>

inline-sizeにはテキストの行長を指定します。

  • 両端揃えができない
  • 日本語は行頭・行末禁則以外では基本的に改行可能と判断される

また、見出しのような箇所で使う場合、次のことを意識しておくとよいでしょう。

今まで手動でやっていた行分割を自動処理にする都合上、特に、本文でない箇所での行分割規則は意識しておかなければなりません。
言語処理が日本語になっていれば(xml:lang=”ja”が適用されるような箇所であれば)
意図してそうしない限り「ー」は先頭に来ませんが、「リ」はそうではないので、

Formatter V7.3 リ
リース!

となるかもしれません。

特定単語の行分割を防ぐため、その箇所のマークアップをtspanにして、分割禁止のプロパティを付けることにします。
「tspanを使うなら結局SVG 1.xとあまり変わらない?」と思われるかもしれませんが、座標の明示がなくなるので処理はかなり単純化します。
「この単語で行分割されるのは困るが、ここ以外であれば別に構わない」というケースが多いとみているのですが、どうでしょうか。

<!-- <heading>Formatter V7.3<keep>リリース!</keep><keep>新機能</keep>と<keep>利用シーン</keep></heading> -->
...
<xsl:template match="heading">
  <svg:svg viewBox="0 0 1920 1080" >
  ...
    <svg:style>
      svg|tspan.keep {
        word-break: keep-all;
      }
    </svg:style>

    <svg:text x="1800" y="100" text-anchor="end"><xsl:apply-templates mode="#current"/></svg:text>
  </svg:svg>
</xsl:template>
...

<xsl:template match="keep">
  <svg:tspan class="keep"><xsl:apply-templates mode="#current"/></svg:tspan>
</xsl:template>

上記は実際に使用したコードではないため、参考程度にお考えください。
inline-sizeが対応するalign調整はtext-anchorによるものになるため、両端揃えの自動配置はできません。

その他、SVGのテキストを使う場合の注意点として、FOのブロックやHTMLのテキストと異なり、
「viewBoxは伸長しないため、想定よりテキストが長くなると表示が見切れる」という点があります。
つまり、「ページ単位で表示領域を確保できる」など、特に高さ・テキスト長があらかじめ想定できる
範囲で使うようにするとよいでしょう。

そして、Formatter V7.3で対応したSVGフィルタ・マスク機能と組み合わせることで文字列に効果を付加できます!

SVGテキストとマスク

画像に対し、テキストをマスクとして被せたものになります。このSVGではtext-anchor=”middle”です。
実のところ、SVGマスク以外の方法もあります、ただ、SVGマスクやフィルタは汎用的ですし、追加の独自仕様無しでXSL-FOでも使えるというメリットがあります。

DITA-OTでソースコードを書くならcoderefが便利

DITAでソースコードを書くときはcodeblock要素を使います。

(HTML5では引用ブロックやcodeblock(pre/code)もfigureの子孫として記述する方法がよく見られます。個人的な感覚として、英語だとキャプションに「figure 1」のようにしてソースコードが記載されていても違和感はないのですが、日本語で「図1」となっているところにソースコードが記載されていると違和感があります。)

さて、codeblockの中をどう書くかについて、方針はおおむね次の2つです。

  • 直接書く。XMLの<や>は
    • &lt;のようにして書く
    • xml-mentionドメインのタグを使って書く
  • coderefを使う

今回は記事タイトルにもあるように、coderefを使う方法が便利という話です。

DITA-OTではcodeblockでの処理について、仕様から拡張しています。記事タイトルが「DITAでcodeblockを書くときは~」ではないのはDITAの仕様ではないからです。なお、記事を作成する際に試行した環境はDITA-OT 3.7.1となります。

Extended codeblock processing DITA-OT

拡張内容は幾つかあるのですが、先に述べた通り、今回紹介するのはcoderefについてです。

codeblockにはテキストをそのまま記述することもできますが、XMLタグ、というか<などがきっちり処理されてしまうため、XMLやHTMLをソースコードとして例示するのは結構大変です。そこでcoderefです。

coderefは外部ソースコードを(主にテキストとして)参照し、結果を展開してほしい場合に使うタグです。@hrefで参照する先を指定します。XMLを参照する場合、@format=”xml”を付けましょう。

coderefの第一の利点は<をエスケープしなくて済む点です。これについてはDITA仕様のうちです。

coderefを使ったコードブロック
<codeblock xml:space="preserve"><coderef href="hoge.xml" /></codeblock>

ただ、実際のXMLファイルというのは結構行数が嵩みます。「coderefで参照する用にコード片を別ファイルに保存して……」というのはメインテナンス性からするとあまり歓迎できません。そこでDITA-OTの機能によって行数を制限します。

coderefの拡張記法は#から続くフラグメントによるものです。これに対応していない、DITA-OT以外のDITA処理系で使われても、ファイルの全行が出力されるだけで済みます。……結構大変なので、keyrefで切り出して切り換え可能にしておくのが良いかもしれませんね。

行数の記法はドキュメントにある通り、#line-range(<start>,<stop>)またはRFC 5147の記法で#line=<start><end>のようにして開始行、終了行を指定します。

このままで十分便利ですが、「元のソースコードを弄ったらトピックファイルで指定している行位置も変更しなくてはいけないのだろうか」と疑問を持たれたことでしょう。それはあまりメインテナンス性が良くありませんね。ということで、任意文字列を行位置の識別子にする方法が提供されています。
#token=<start-text>,<end-text>を指定すると、ソースコード中のstart-textがある行の次行からend-textがある行の前行までが範囲として取り出されます。想定としてはコメントアウトした行にstart-text、end-textを書いておく形のようなので、あまりトリッキーなことはしない方が良いでしょう。ほかにも幾つかの機能がDITA-OTのページで紹介されていますが、プラグインや処理系依存の機能もあるようなので都度確かめて使うと良いでしょう。

coderefのstart,end用文字列を追加したXML
<!-- example1start -->
<fo:block><fo:inline>Title</fo:inline></fo:block>
!-- example1end -->

ほか、coderefというcodeblockの中で更に別のタグを使うことのメリットは、@hrefで参照した箇所と、直接書く箇所をcodeblockの中で行える点です。

coderefを使ったコードブロック

<codeblock xml:space="preserve"><coderef href="hoge.xml#line-range(1,5)" />
... <!-- 直接書いた部分 -->
<coderef href="hoge.xml#line-range(10,15)" /></codeblock>

1-5行目、「…」を書いて10-15行目、なんて表示も可能になります。

そんなcoderef、DITA 2.0で若干の変更が入ることが現在のドラフトで言及されています。といってもエンドユーザがトピックを記述する上ではそう変化はなく、主に仕様上の立ち位置がより整理されるということのようです。

『Office Open XML Formats入門 第2版』を制作しました

2021年12月07日 16:00~17:00 に「ちょっと一息アンテナハウスウェビナー『Office Open XML Formats入門 第2版』制作報告」を発表しました。販売、公開よりも発表が先になってしまったため、内容が気になっていた方もいらっしゃるのではないでしょうか。

こちらが表紙画像です。

Amazon POD用表紙画像
2021年12月07日ウェビナーのバナー

組版をAH XSL Formatter V7.2で行ったため、『AH Formatter XML関連出版物の紹介』ページに掲載しています。

Amazonの販売ページへのリンクは次の通りです。

https://www.amazon.co.jp/gp/product/4900552836

また、本書のPDF版は弊社オンラインショップからご購入いただけます。印刷版とレイアウトの微調整を行いました。

https://web.antenna.co.jp/shop/html/products/detail.php?product_id=1301

HTML版はOffice Servers資料室のページからご覧いただけます。


ウェビナー概要や書籍紹介ページにある通り、アンテナハウス『Office Open XML Formats入門』の初版は2007年に出版社から刊行されました。
今年は2021年、つまり14年程前の書籍の改訂版ということになります。
内容的な修正は必要とはいえ、以前の版の原稿をベースに新たな版を制作するというとき、XML原稿はほとんど変更が必要ありません。以前の版の構造に不満がある場合はその限りではありませんが。

一方で大規模な修正の余地があるのがXSLTでした。そしてウェビナー(と書籍の後書き)では、XSLTについてはかなり省いて説明することになったため、本記事ともう一度どこかで補足することにしたいと思います。

2007年というのは、XSL的にもそこそこ大きな節目でした。XSLT 2.0のW3C勧告です。XSL 1.1の勧告は2006年でしたが、実利用として熟れていないという点では二者とも同様です。XSLT 2.0以降はミスの発見やスクリプトの見通しにおいてXSLT 1.0とは別言語に近い体験をもたらします。msxmlのXSLTが1.0であることや各ウェブブラウザほか処理系の多くが1.0までしか対応していないことも手伝って、使える状況が限られるのは悩ましいところです。

変更の概略

今回は自社事例でしたので、初版で1.0だったXSLTを3.0に書き換えました(完全に3.0向けに最適化したとはとても言えませんが)。

書籍初版の制作報告にもあったように、XSL 1.1での大きなポイントにbookmarkのとindex関係の語彙が入ったことが挙げられます。『Office Open XML Formats入門』初版では、bookmarkについてはXSL Formatterの拡張仕様、索引についてはbasic-linkとXSLTによる力技による解決が図られていました。

(AH )XSL Formatterの拡張仕様では実際のフローコンテンツ登場箇所と同一の箇所に記述するため、処理においてXML中の章構造に当たったときに同時に処理すれば良いことになります。よってXSLT記述としてはbookmark-tree用に処理を追加するよりも単純に書けます。XSLT 1.0で書く場合は複数回ドキュメントを走査することが難しいということも手伝っていたのかもしれません。

索引構造については、XSL 1.1のindexを導入することで、XSLTで行わなければならなかった処理が簡略化されます。索引語に当たったときの処理で、「既に同じ索引語が登場しているか」「同じ索引語が同ページに登場したときにページ数表示を合一する」といった判別がXSLのプロパティで変更可能になるため、分岐処理などが大幅に簡略化できました。

割とアドホックな書き換えを行っていたり、徹底できていなかったりするため、引き継ぎをするにはリファクタリングが必至ですが、大体このようなことを行いました。

  • テンプレート中に直接書き込まれたattributeをattribute-setへまとめる
  • 共通処理をまとめる。
  • apply-templatesを含まないような分岐は名前付きテンプレートへ追いだし、パラメータを渡すようにする
  • XPathによる値の取得はできるだけテンプレートの先頭でまとめ、利用箇所ではselect="$value"のように呼び出すだけにする

先に書いた通り徹底はできておらず、私自身も、テンプレート中で直接attributeを指定しているような箇所をかなり生み出してしまいました。とはいえ、ギリギリの時期に「ヘッダーの位置をもう少し下げて」といった指示に1行の変更だけで対応できるようにはできたので、無駄ではなかったと思います。

実は上に挙げたものはXSLT 1.0時点でも時間さえあれば行える変更で、3.0への変更には関係ありません。値のみを取得、操作したい箇所をfunction化したり、処理をreplace()関数に書き換えたりといった作業は2.0から行えます。3.0としては関数の括弧が入れ子ではなくarrowを使えたり、文字列結合に「||」を使えるといった枝葉の変更点を使用しています。

参考資料

XSL-FO 試行錯誤 カレンダーを自動生成したい(構想編)

師走まで間もなくとなり、日中の気温もかなり下がってきました。来年のカレンダーを用意する時期ですね。
ちょっと「XSL-FOでオリジナルのカレンダーを作りたい」なんてこともあるのではないでしょうか。

高々12月分であるのでDTPソフトウェアやXSL-FOの直書きで頑張っても何とかなるかもしれませんが、来年もある程度使い回せるようにしたり、2021年のように直前の変更があるかもしれないことを考えると自動化したいところです。

実現したいカレンダーは次のようなものを想定します。手書きであるため半端なところまでしか日付がありません。

実現したいカレンダーのラフ

今回は構想編ということで、方針や使っていくFO、XSLTのアタリを付けていきましょう。

ページレイアウト

カレンダーを作るにあたって、先ず決めなければいけないことは「どの程度グラフィカルにするか」、具体的にはページレイアウトをどこまで制限するか、ということです。
個別箇所の自由度を上げるとその分だけ自動化できる箇所は少なくなります。

とはいっても、「高々12月分」と書いたように12パターン程度であれば12通りのページレイアウトを用意しても良いでしょう。今回はページレイアウトを1つに定めることにします。

  • ページレイアウトは1つ
  • 毎月1ページで構成

(スケジュール帳を組むのであれば「1月分の中にページ分割はあるか」も考慮しなければなりません。AH XSL Formatterであれば見開き要素が使えるため比較的簡単に対応できます。その場合は、見開きの左右に分割される位置と格子の位置を上手く配置する工夫が必要になりますね。)

ページサイズはA3縦(縦420mm、横297mm)にしましょう。

毎ページ登場する内容

月ごとにページを分けるのであれば、各ページに共通して登場するのは
年でしょうか。来年であれば「2022年」ですね。これはstatic-contentに置くことにします。
また、年度(4月始まり)で設定したい場合、途中で年の表示は変わりますね。このことに備えて、マーカーで取り出した値を使うことにします。

後述するように画像を上半分にページ幅一杯に表示する際、画像の上にstatic-contentを被せて表示することが可能です。AH XSL Formatterではregion表示の優先順位も自由が利きます。

マス目表示を実現するFO

ここは奇をてらわずtableを使っていきます。

カレンダーでは、曜日をカラムのタイトルとして、1週目、2週目、…をrowのまとまりとして、それぞれの日付がセルとして表されます。
常にマス全体を長方形にすることとして、rowの数は5週分にします。

例示した画像のように日付の始まりと曜日の始まりが一致するなら並べるだけなので楽ですが、そうではないためXSLTで自動化することになります。当月の他、その先月または来月の数日間の情報が必要になることも、お手元のカレンダーから想像が付くのではないでしょうか。

元となるカレンダー用のXMLがある場合は、そこにテンプレートを適用するだけで済むでしょう。

日付の位置を調整して自動生成するXSLT

XSLT 2.0からは日付のための関数や型を利用できます。ある日付の曜日を取りたいときはたとえばformat-date('[w1]')とすればxs:integer型ではありますが曜日が取得できます。このことと日付用のduration、そしてmodを組み合わせればカレンダーのマス目が実現できそうです。

休日・祝日(今回は対応しない)

休日、祝日の背景色を変更することについて考えます。日曜を示すカラムが先に決まるため「毎週日曜の背景色を変更する」であればカラムに対して設定したりすれば良いでしょう。問題は祝日です。XSLTの標準ライブラリは日本の祝日対応まではしてくれていません。確実なソースとなると、内閣府のページにあるCSVでしょうか。

「国民の祝日」について – 内閣府

XSLT 2.0以降でCSVをパース可能なようにプログラムを書くこともできますが(<xsl:analyze-string>などを使います)、とりあえず今回は対応しないことにします。

そのほか二十四節気なども同様に、「どこかから日付と紐付いた外部ソースを得る」「XSLTで取り込んで展開する」といった手順になるでしょう。

各月のイラスト、写真の設定方法

話をFOメインに戻します。

ページレイアウトは1つだけにすることを決めました。
AH XSL Formatterではページマスターに背景画像を設定することも可能ですが、
1つのページレイアウトを使い回すのであれば、フローコンテンツとして指定していく形になるでしょう(実はマーカーに画像を指定することも可能ですが、テンプレートの記述量は大して変わりません)。

画像のクリッピングや位置調整については背景画像の方がプロパティ指定の余地が広いため、ブロックコンテナーの背景画像として配置することにします。

画像をページギリギリまで表示したい場合、フローコンテンツを塗り足し領域まで表示させる必要があります。この辺りのTipsは『AH XSL Formatter 拡張仕様使いこなしガイド』に載せていたりします。

次回予告

次回はカレンダーを実現するための関数、テンプレートについて考えていく予定です。

参考資料


XSL-FO試行錯誤 スライドPDFをXSL-FOで組む

以前に、スライドをXSL-FOで作成した例をご紹介しました。
今回、明日9/17(金)17時よりオンライン開催される「FormatterClub2021」用のスライドを改めてXSL-FOで作成したのでご紹介します。

まずは明日の宣伝です。

FormatterClub2021ウェビナー

【どなたでも無料視聴可能】

日時
2021年9月17日(金)17:00~18:10
概要
今回のFormatterClubでは、キヤノンの吉田一様にFormatterでのマニュアル作成と自動組版の取り組みを発表していただく他、XSL拡張仕様のご説明、AH Formatterの今後をご紹介を致します。
内容紹介・お申込みページ
ウェビナー登録ページ

今回はPowerPointで下書きを作成した後、清書をXMLで行い、XSLTでXSL-FOに変換するという方法を取っています。以前のウェビナーではPowerpoint原稿をそのまま使ったのですが、見出しや各箇所の文字サイズといった、同じ内容の繰り返しが多い事柄に対し個別に変更を行っていくのはちょっと辟易してしまったためです。
Powerpoint文書もXMLの集合ではあるのですが、PresentationMLはとくにDrawingML色が強く、いってみればグラフィカルな配置に重点が置かれているため、「流し込んだら良い感じに自動配置してほしい」といった要求のための元テキストを得るのは恐らくWord以上に困難です。

XSLTのバージョンは3.0で動かしています。記事中に登場するFO、XSLTの動作について保証するものではありません。ご了承ください。

XML

次のようなXMLからXSL-FOを生成します。

<section xml:id="about"><title>概要</title>
   <div style="slide" layout="cover" />
 ...
   <div style="slide" layout="fig-desc">
   <description>
     <dl>
       <dlentry>
         <dt>書名</dt>
         <dd>AH XSL Formatter拡張仕様使いこなしガイド</dd>
       </dlentry>
       <dlentry>
         <dt>公開日</dt>
         <dd>2021年5月18日</dd>
       </dlentry>
       <dlentry>
         <dt>概要</dt>
         <dd>XSLをAH XSL Formatterが独自に拡張した仕様についてのガイド
           <list style="unordered">
            <li>複合的なショウケース</li>
            <li>「やりたいこと」から引ける逆引きリファレンス</li>
          </list>
        </dd>
      </dlentry>
     </dl>
   </description>
   <figure>
      <image href="./XSL-FO-Extensions.png"/>
   </figure>
 </div>
 <div style="slide" layout="plain">
 ...
 </div>
</section>
...

若干の制限を設けたブロックとしてdescriptionタグを用意していますが、概ねHTML5程度の表現力のXMLを用意しました。汎用タグdivで何でも表そうとすると、子や親の要素・属性に制限を設けることが難しいという欠点にぶつかります。

dlentryはdtとddの組を区切るのに使用しています。HTML5のようにdivでないのは上と同じ理由です。区切りを前提にすることによってXSLT上でのパースがかなり楽に行えます。

ulとol相当はlistのclassとして使い分けます。CommonMarkのDTDなどはこのように扱っていますね。

テーマ擬き

Powerpoint、あるいは多くのプレゼンテーション用ソフトには「テーマ」という、スタイル設定の集合があります。
Word文書などと同様、Powerpoint文書はレイアウト用と見た目用の構造が渾然一体となっているのでテーマ切り替えだけでは大抵万事上手くは行かず適宜調整することになります。とまれ、見た目上の違いが印象に大きな影響を与えるスライド文書では類似の機構があると便利です。

今回は次のレイアウトを用意しました。

  • 「表紙用レイアウト」
  • 「大項目扉ページレイアウト」
  • 「通常ページレイアウト」
  • 「段組レイアウト」
  • 「画像とその説明用レイアウト」
  • 「大きめの画像表示レイアウト」

大きく異なるレイアウトについては異なるページマスターとして用意します。
通常ページ、段組、画像とその説明、大きめの画像表示レイアウトは同一のページマスターです。また、スライド全体の表紙と、大項目扉ページは同一のページマスターです。両者の違いは、fo:region-start、fo:region-before、fo:region-afterの領域の有無、またそれを考慮したfo:region-bodyであるかです。

表紙系のページ
表紙以外のページ

(AH XSL Formatter拡張仕様ではページシーケンスの入れ子が行えるので「同じ項目内容の途中でスライドのレイアウトを切り換える」という要求を簡単に満たすことができます。この辺りも元のXML構造の設計に影響します。)

表紙系でないページでは、小項目のタイトルをfo:region-beforeに表示することにします。スライドではヘッダーとしての表示と見出しとしての表示を兼用してもあまり違和感がありませんね。また、大項目をfo:region-startに配置します。16:9のスライドの場合、画面の左端から右端までが4:3に比べ遠くなるため、タイトル領域として活用することにしました。こうすることで、大項目と小項目を両方表示したいとき、1つのヘッダーに大項目と小項目を両方表示するよりも、あまり文字を小さくせずにすみます。

段組レイアウトはAH XSL Formatter拡張仕様であるブロックコンテナへのcolumn-count指定で実装しました。

画像とその説明用のレイアウトはインラインコンテナを並べて実装しています。インラインコンテナは分離しないよう、念のためkeep-with-*などを指定します。幅指定を行わないと2つめのインラインコンテナが次の行へ送られてしまったり、高さ指定を合わせておかないとline-stacking-strategyによっては配置がガタガタしてしまいますから注意が必要です。

画像とその説明用のレイアウト
<fo:block>
 <xsl:attribute name="line-stacking-strategy">line-height</xsl:attribute> 
   <fo:inline-container width="48%"
     height="100%"
     keep-with-next.within-line="always">
     <fo:block>
        <xsl:apply-templates select="child::description"/>
     </fo:block>
   </fo:inline-container>
   <fo:inline-container width="50%" height="100%">
     <fo:block>
       <xsl:apply-templates select="child::figure"/>
     </fo:block>
   </fo:inline-container>
 </fo:block>

ほかの方法としては、「region-bodyを複数用意してflow-nameを切り換える」、「画像を左右へのフロートで配置する」といったものが考えられます。フロート配置は王道ですが、スライド用途の場合、AH XSL Formatter拡張でなければ思ったように(目的のスライド位置に)配置することはかなり難しいかもしれません。

箇条書き

スライドでは箇条書きが頻出する傾向があります。端的に内容を示し、口頭でのプレゼンに詳細内容を譲るためなど理由は考えられますが「様々な場所で登場する」ということに留意する必要があります。

XSL-FOのリスト構造はstart-indentとend-indentが肝要です。

ブロックのインデントは基本的には親の値を引き継いでいれば問題ありませんが、start-indentを指定したブロックの入れ子にリストブロックがあるケースなどでは、うっかり領域の端からの値にならないよう、inherited-property-value(*-indent)で親のインデントの値を取得してから自分のインデントを追加する必要があります。

<!-- 入れ子レベル$listLevelは計算済 -->
...
<xsl:attribute name="provisional-distance-between-starts">
 <xsl:value-of select="'inherited-property-value(start-indent) + 0.5em + ' || $listLevel || 'em'"/>
 </xsl:attribute>

fo:list-item-labelの内部はブロックなので、ラベルに使える記号の自由度は高いです。今回は入れ子レベルによるラベルの切り換えは行いませんでした。

定義箇条もリストブロックにしました。同じDL内のDTの文字列長を比較し、最長のものを基準にインデントを決定しています。あまりにDTが長ければ単純なブロックに切り換えます。今回は和欧混合文などは考慮しません。


<xsl:template match="dl">
 <xsl:variable name="dt-lens">
   <xsl:for-each select="child::dlentry/dt">
     <len>
        <xsl:sequence select="xs:double(string-length(./text()))"/>
     </len>
   </xsl:for-each>
 </xsl:variable>
 <xsl:variable name="max-length-dt" select="xs:integer(max($dt-lens/len/text()))"
   as="xs:integer"/>
 <xsl:choose>
   <xsl:when test="$max-length-dt &lt; 20">
     <fo:list-block
       provisional-distance-between-starts="inherited-property-value(start-indent) + {$max-length-dt}em + 2em"
       provisional-label-separation="1em">
        <xsl:for-each select="child::dlentry">
           <fo:list-item>
             <fo:list-item-label ...">
                <fo:block ...>
                   <xsl:apply-templates select=".//dt"/>
                </fo:block>
             </fo:list-item-label>
             <fo:list-item-body ...>
                <fo:block ...>
                   <xsl:apply-templates select=".//dd"/>
                </fo:block>
             </fo:list-item-body>
           </fo:list-item>
         </xsl:for-each>
     </fo:list-block>
   </xsl:when>
   <xsl:otherwise>
    ...
   </xsl:otherwise>
  </xsl:choose>
</xsl:template>

最長である「公開日」の長さを基準にインデントが決定されています。均等割り付けの指定は「:」を含んでしまっているので「概要」の後のスペースなどはちょっと微妙です。end方向に揃えるか、axf:text-autospaceで揃えるか、ブロックの横にフロートかブロックコンテナで「:」配置すればより良くなるでしょう。

枠内にテキストを収める

大項目の扉ページでは、見出しテキストを四角い枠内に収めることにしました。

スライドは本と比べ、ページごとの独立性が高くなることが多いでしょう。「1枚の中に必要なコンテンツを収めたい」というとき、フォントサイズや字間、伸縮などを最適なものから外しても1枚のスライド内に収めるため、オーバーフロー処理を記述します。

どうにかテキストを枠内に収めるにしても限界値はあります。より汎用的に使うのであれば、文字数制約や、枠自体のサイズを指定し直すような記述を追加することになるでしょう。


今回採用、実装しなかったものとしては「自由配置の吹き出し」や「画像の上から付ける下線」などがあります。グラフィカルな配置はスライドとしても重要な要素ですが、代用が可能なものでもあります。たとえば画像側で下処理を行っておけば、清書としては都度処理する必要はありません。
とはいえ、まったくそれらの処理ができないのも不便ではあります。
今後、このようにXMLからPDFスライドを作成する機会があれば、SVGやブロックコンテナをページ上の座標など簡単なパラメータで任意の場所に配置するような形態で実装するかもしれません。

関連記事


【動画公開】「DITAで本を書いてAH XSL Formatterで自動組版する」、FormatterClub2021ウェビナーのお知らせ

2021年8月10日に開催しました、ちょっと一息アンテナハウスウェビナー「DITAで本を書いてAH XSL Formatterで自動組版する」の録画を編集した動画が公開されています。

動画は1本10分程度の4本立てとなりました。

ご好評いただけましたようで、当日は予定時間をはみ出すくらいにご質問をいただきました。
「タグを気にせず書きたい」といった意見もございました。個人的には「タグを気にしながら書きたい」派でしたのでそういった方のための資料はあまり用意できていなかったのですが、そういった方面のアプローチも存在しますので、機会があればあらためてまとめたいと思います。

さて、この動画自体は少し前に公開されたのですが、何故今ブログ記事にしているかというと、次のイベントに関連するためです。

FormatterClub2021ウェビナー開催

日時
2021年9月17日(金)17:00~18:10
概要
今回のFormatterClubでは、キヤノンの吉田一様にFormatterでのマニュアル作成と自動組版の取り組みの発表の他、XSL拡張仕様のご説明、AH Formatterの今後をご紹介を致します。
内容紹介・お申込みページ
ウェビナー登録ページ【終了しました】

Formatter Club は、アンテナハウスの XML 自動組版ソフト『AH Formatter』を導入されているユーザーや関心を持っていただいている皆様と開発者とを繋ぎ、会員同士の交流、情報交換により『AH Formatter』の利用技術を向上させ、より皆様のお役に立てる製品としていくために役立てることを目的として発足いたしました。
『AH Formatter』とその関連技術(XSL、CSS、XML 多言語組版など)に関心のある方でしたらどなたも参加できます。参加費は無料です。
Formatter Club の活動には会員メーリングリストを情報交換のために利用し、会員専用の Web ページも提供いたします。また定例会を開催し、会員相互の親睦をはかります。定例会は、『AH Formatter』開発者から最新バージョンの紹介・デモ、活用事例紹介、組版技術の向上のための勉強会などを行います。

Formatter Club について | アンテナハウス株式会社

ということで、「XSL拡張仕様のご説明」として『使いこなしガイド』の紹介をさせていただく予定です。
今回のウェビナーはFormatter Club会員様以外でも参加が可能ですのでお気軽にご登録、ご視聴ください。

関連記事


XSL-FO試行錯誤 変換後を意識しながら独自XMLを考える

業務ではなくプライベートの話となるのですが、自分用のXML規格を考えたり、そのXMLを変換するXSLTを書いたりということをしています。

自分用のXMLはさらに自分用の軽量マークアップからの変換によって得ることを想定しているので、スキーマまで作りこもうと日々ジリジリと進めています。

XSLTは組版用でないXMLを組版用のXSL-FOに変換できるわけですが、「軽量マークアップとして書きやすく、XSL-FOやHTMLにもしやすい」XMLというのはなかなか難しいなと感じています。

脚注はDITAではfnタグでマークアップします。XSL-FOではfootnote、footnote-bodyに変換されるでしょう。CSS Paged mediaではspanにした後フロートさせることが多いようです。
Webページ、HTML5的には脚注文の位置はasideの中あたりが良いのでしょうか?ただ、HTMLページの一番下(フッタ)に置いてしまうとPDFや紙でのソレよりも遥か下に行ってしまい、後注、しかもappendix的な配置になってしまうかもしれません。
では脚注用のfootnoteタグを用意するのではなくnoteタグを用意してレイアウトは属性にしましょうか?

変換先がXSL-FOであれば出力は1ファイルが望ましいでしょう。
しかしたとえばDITAなら?元の独自XMLはマップやトピックをどのように扱っていると好ましいでしょうか?独自XML自体が外部ファイルをどう扱うかも重要です。XIncludeなどを援用する、しない。うーむ。

メインの構造をまたぐ構造、たとえば複数段落に対する注釈などについてはrange-startとrange-endのようなタグを作る方式である程度しのげますが、変換後に納得できる構造にできるかは悩ましいところ。

そんなことを考えていると休日が過ぎています。
XMLアプリケーションはそれぞれがそれぞれの用途のために専門化されているので、中間形式のための独自XML規格というのは実は難しいのかもしれません。

DITAで本を書いてAH XSL Formatterで自動組版する

日時
2021年8月10日(火)16:00~17:00
概要
2021年5月18日に公開/販売した『AH XSL Formatter 拡張仕様使いこなしガイド』の制作報告を通し、XML執筆からPDFを作る過程の知見をご紹介します。
DITAについてや、DITAでの書籍制作における実例の紹介や、DITAを扱うときの注意事項など、自動組版やXMLの使い方、DITAに興味がある方、Formatterユーザーさん、必見です!
内容紹介・お申込みページ
こくちーずプロからお申し込み:https://www.kokuchpro.com/event/20210810/終了しました
Zoomウェビナーへ直接お申込みいただく場合: ウェビナー登録ページ終了しました

XSL-FO試行錯誤 SVGでもっとグラフィカルなボーダーを

Antenna House Formatterではボーダーに指定可能な線種について大きく拡張されています。

しかし、それでもボーダーの意匠について自由自在とまでは行きません。
例えば「ロープで結ったような枠線を出したい」というときには、背景画像を置くことになるでしょう。ボーダーの扱いで(他の多くと同様に)難しいところは、ページ分割のときにどうするかです。
背景画像に固定枠の画像を指定したとき、段落の長さによって枠をずらすようなことはできませんね*。ページ分割時にbeforeとafter部分のボーダーを再表示する必要がなければ、段落の前後に画像を挟めば良いだけで、テーブルレイアウトにする必要はありません。

さて、XSL-FO試行錯誤シリーズ、以前の回でテーブルレイアウトを駆使してブロックがページ分割されるごとに特定の要素を出現させる方法を紹介しました。
つまり、テーブルレイアウトのヘッダとフッタにボーダーとなる画像を指定し、テーブルのボディのbeforeとafterの位置に表示させてやろうというのが今回の試みです。

やることは単純明快。ソースコードは単純とはいきませんが。
早速結果を見てみましょう。

可変長の段落に対応した装飾的なボーダー

今回のソースコードは(特に)色々無駄があります。見ながら一緒に突っこんでいただければ幸いです。

ソースコード
<fo:block-container>
  <fo:table table-omit-header-at-break="false" table-omit-footer-at-break="false">
    <fo:table-header>
      <fo:table-row>
        <fo:table-cell>
          <fo:block-container 
            absolute-position="absolute"
            top="-2cm" left="-2.6cm"
            width="100%" height="6cm"
            axf:background-content-height="scale-down-to-fit"
            background-image="url(./corner.svg)"
            axf:background-size="cover">
          </fo:block-container>
        </fo:table-cell>
      </fo:table-row>
    </fo:table-header>
    <fo:table-footer >
      <fo:table-row>
        <fo:table-cell>
          <fo:block-container 
            width="100%" height="10cm">
            <fo:block-container
              absolute-position="absolute"
              top="-4cm" left="3cm"
              width="100%" height="10cm"
              axf:background-content-height="scale-down-to-fit"
              axf:transform="rotate(180)"
              background-image="url(./corner.svg)"
              axf:background-size="cover">
           </fo:block-container>
         </fo:block-container>
       </fo:table-cell>
     </fo:table-row>
   </fo:table-footer>
   <fo:table-body>
     <fo:table-row>
       <fo:table-cell>
         <fo:block space-before="4cm"
           space-before.conditionality="keep" start-indent="2cm" end-indent="2cm">
 Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

...
 
         </fo:block> 
       </fo:table-cell>
     </fo:table-row>
   </fo:table-body>
  </fo:table>
</fo:block-container>

思いつきのままに書いたのでもっとやりようがある気もします……。

corner.svgがボーダーとして用いる画像です。上のコードのように、わざわざaxf:transformで回転するよりも、もう1つフッタ位置用の画像を用意した方が早いでしょう。

ボーダー分、段落のbeforeにスペースを空けています。ボーダー画像を背景に指定しているブロックコンテナにabsolute-positionを設定しているためで、ブロックとfo:external-graphicでボーダー画像を配置した場合は不要です。SVGのサイズをきちんと把握して作成しておけば、もっとスッキリした指定になるでしょう。
background-sizeにcoverを指定していますが、coverである必要もないですね。左上の画像は高さの指定と合わず切れているので怒られかねません。

今回はbeforeとafterのボーダーに注目しました。ではstart、endのボーダーはどうすればいいのでしょうか? これは実はより単純で、背景に指定したボーダー用の画像をbackground-repeatでブロック進行方向に繰り返すことで、ある程度可変長の段落に対応できるようになります。ただ、ボーダー画像の端で上手く繰り返せるように段落の高さが背景画像の高さの整数倍になるようにする、beforeとafterのボーダー画像と破綻しないようにする、といった調整が必要になるでしょう。
beforeとafterについては背景画像ではなくfo:external-graphicやfo:internal-graphicでもあまり問題はないのですが、startとend方向は繰り返しの指定上背景画像への指定が良いのではないでしょうか。

* 後から検討したのですが、背景画像を指定した複数のブロックコンテナを入れ子で記述して描画位置を調整すればある程度可能そうです。結局ページ分割の問題にあたってしまいますが。

DITAで本を書いてAH XSL Formatterで自動組版する

日時
2021年8月10日(火)16:00~17:00
概要
2021年5月18日に公開/販売した『AH XSL Formatter 拡張仕様使いこなしガイド』の制作報告を通し、XML執筆からPDFを作る過程の知見をご紹介します。
DITAについてや、DITAでの書籍制作における実例の紹介や、DITAを扱うときの注意事項など、自動組版やXMLの使い方、DITAに興味がある方、Formatterユーザーさん、必見です!
内容紹介・お申込みページ
こくちーずプロからお申し込み:https://www.kokuchpro.com/event/20210810/
Zoomウェビナーへ直接お申込みいただく場合: ウェビナー登録ページ


XSL-FO試行錯誤 最初からハイフンのある語の制御

5月18日に『AH XSL Formatter拡張仕様使いこなしガイド』が公開・発売したことは
当日の記事で述べました。

『Antenna House XSL Formatter 拡張仕様使いこなしガイド』を公開・発売しました I Love Software2!

『XSL-FO 試行錯誤』連載ではこれまで密かに「AH XSL Formatterの拡張仕様はメインの対象にしない」という縛りでやってきたのですが、めでたく書籍が公開されましたので、これからは拡張仕様についても対象にしていきます。『AH XSL Formatter 拡張仕様使いこなしガイド』に載せられなかった話題について扱うかもしれません。たとえばAH XSL FormatterではCSS3にあるような装飾表現が可能であったりするのですが「CSS3を紹介したWebサイトを見れば良いのでは」となってボツにしたものがそこそこあります。

制作のXSL-FO以外の面についての話についても折を見て記事などで展開する予定です。気になった点などがあればコメントやSNSなどで言及していただければ反映できるかもしれません。

最初からハイフンのある語の制御

ということで、本記事の話題は見出しの通り。どういったものがあるかというと、コマンドラインのオプション引数(-optionとかですね)、XSL-FOの要素、プロパティ名など(border-top-styleなど)ですね。

そもそもこれらは行分割位置に来ないようにするのがおそらく理想的ですが、ページ数の制約などで妥協することもあるかもしれません。

もとからハイフンがある語は分割によるハイフンを追加しない

<fo:block ...
 hyphenate="true"
 axf:hyphenate-hyphenated-words="false">...
</fo:block>

サンプルFOのページのサンプルでもよく使われています。プロパティ名など、もともとハイフンを含む語にハイフンが追加されてしまうと、元からあったハイフンなのか、分割時に追加されたハイフンなのか分からなくなってしまうかもしれませんからね。

他の回避手段としては、ハイフネーションとして追加される記号を変更するという方法も考えられます。

ハイフンを含む特定の文字列を、分割可能でないハイフンに置き換える

(XSL-FOの標準仕様として、「行分割でハイフンが追加され得る単語で、分割前後の文字数によって分割位置を調整する」ことができるhyphenation-remain-character-count、hyphenation-push-character-countがあります。)

さて、元からハイフン(-)を含む語があり、初期設定でこのハイフンの後では分割可能であるとします。しかし、この文字の後が1字のみのとき(「UTF-8」など)のとき、「8」だけ次の行にいくのはあまり望ましくないですね。

「分割しないハイフン(NON BREAKING HYPHEN)」(U+2011)という文字があります。この文字でハイフンを置き換えてあげれば、ハイフンの前後で行分割は通常起こりません。
では、すべてのハイフンを置換しますか? それはそれで文字数の調整が狂ってしまいかねません。
また、元のファイルが500ファイル以上になっていたので、時間的に個別に置換するのはちょっと困難だとします。

今回は「特定の文字の組み合わせ(-*)でのみ、1字の次行送りが発生する」ことが分かっていたので、axf:text-replaceを使うことにしました。

axf:text-replace / CSS (-ah-)text-replace AH Formatter マニュアル


<xsl:attribute-set name="code">
<xsl:attribute name="axf:text-replace" select="'-&#x2217;' '&#x2011;&#x2217'"/>
</xsl:attribute-set>

指定に確認していない漏れがなければ上手く行っているはず。


この成果(書籍全体で適用されているか)は『AH XSL Formatter 拡張仕様使いこなしガイド』で確認してください!
(XMLファイルを処理するときにXSLTは経由したわけで、XSLT 2.0の正規表現などによる置換なども可能ですが、その辺りをあまり変更したくない、変更できない場合、ありますよね?)


『AH XSL Formatter 拡張仕様使いこなしガイド』