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-startfo:region-beforefo: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やブロックコンテナをページ上の座標など簡単なパラメータで任意の場所に配置するような形態で実装するかもしれません。

関連記事





Office Server Document Converter
サーバーサイドでダイレクト文書変換!


PDF Driver
印刷からPDFを生成。PDF 変換ドライバー!


瞬簡PDF 変換 2024
PDFをOffice文書へ高精度変換


瞬簡PDF 書けまっせ 2024
PDFに文字が書ける! 入力欄を自動認識