Help us understand the problem. What is going on with this article?

CSS組版を目指して --原稿からPDF作成まで--

技術書典8で「お絵描きソフトをつくる本」という本を発行することにしました。そこで、流行りのCSS組版をやってみたのでログを残しておこうと思います。

CSS組版って

組版は製本をするための工程の1つで、原稿を作成した後に、文章、図などをレイアウトしていく作業です。InDesignなどのDTPソフトウェアを使ってソフトウェア上でレイアウトをしてくことがほとんどです。また、TeXなどのソフトウェアも組版ソフトウェアの1つです。
最近では、原稿をHTMLで作成し、そのレイアウトをCSSで指定する、CSS組版と言われる手法も出てきました。vivliostyleなどのOSSを使うと、簡単にCSS組版を利用することが出来ます。

CSS組版どうやるの?

まずは、やってみた人達のドキュメントを真似するのが手っ取り早いです。
https://vivliostyle.org/ja/samples/
https://vivliostyle.github.io/vivliostyle_doc/ja/vivliostyle-user-group-vol1/shinyu/index.html
https://vivliostyle.github.io/vivliostyle_doc/ja/vivliostyle-user-group-vol1/yamasy/index.html

今回のワークフローの備忘録

いくつか「???」と思ったこともあったので、自分のワークフローのログを残そうと思います。

原稿作成

文章と図の入力

今回は複数人で原稿を持ち寄るので、その時のフォーマットはWordにしました。Markdownでも良いのですが、ページの分量を把握しながら図の位置を入れ込んだり、表を書いたりするのにはやっぱりWYSIWYGは楽だったりします。が、Linuxしか持ってない私が使ったのはWord for Web。
…便利なんですが、ゼロから原稿を作るのには向いてないですね、これ。

ということで、最終的にはWordの文書を.docファイルで保存してから、Google Drive経由でGoogle Document+Google Slideに移行しました。
Google Slideは良いですね。

  • 使いたいショートカットが一通り揃っています。
    • 少し癖はありますが、普通に作図ツールとして使えます。 特に、Ctrl+ドラッグでオブジェクトの複製をするショートカットが使えるのはポイントが高いです。
  • 各スライドをSVGとして出力できます。
    • 画像で出力…まではよくある話ですが、SVGの出力は重宝します。

Google Documentは、感触としてはGoogle Slideほどのインパクトはないですが、やはり地味に良いです。

  • Google DocumentはSlideのページをコピペできます。
    • これ、Officeだとできて当り前じゃん…と思ってたんですが、Word for Webにはこの機能が無いんですよね……
  • 取り込んだ画像をあとからトリミング出来ます。
    • 地味に便利です。HTMLに出力すると「あっ」と気づくんですが、これ、CSSのフィルタに変換されるんですね。 なので、HTMLで読んだ画像を後から編集すると、その編集後の画像をトリミングした状態になります。後述する画像のSVG化で便利です。

ただ、デザインやレイアウトについて言えば、利用できるフォントが致命的に少なかったので、原稿を作る以上の作業をするのは諦めました。

原稿のHTML化

Google Documentで書いた文書をHTMLに変換します。これは、Google Documentの「ダウンロード>ウェブページ」でダウンロードすればOKです。このときにはzipファイルがダウンロードできます。
google-document-html-export.png
ダウンロードしたファイルはこのような構造になっています。

<文書名>.zip
    +--<文書>.html
    +images
       +--image1.png
       +--image2.png
       +--image3.jpg
       ...

HTMLと画像がエクスポートされます。Slideから入れ込んだプレゼンの図もPNGなどに変換されています。ただし、このHTMLファイルは、幾つかの欠点があります。

  • UTF-8の文字列がエスケープされているので編集には向きません。
  • 「1.1 H1のタイトル」と入力したものは<ol><li><h1>H1のタイトル</h1></li></ol>と展開されてしまいます。
  • 改行を入れると、<p><span></span></p>という何もない無駄な構造が大量に生成されます。

チマチマと直すのが非常に面倒です。なので、次のようなスクリプトを作って、HTMLを比較的プレーンなものに書き換えました。

html2html.py
#!/usr/bin/env python3

import sys
import html

from bs4 import BeautifulSoup
with open(sys.argv[1],"r") as f:
    soup = BeautifulSoup(f.read(), 'html.parser')
span = soup.select("p>span:empty")
for i in span:
    i.extract()
p = soup.select("p:empty")
for i in p:
    i.extract()
headers = ["h%d"%i for i in range(2, 5)]
for h in headers:
    hs = soup.select("ol>li>%s"%h)
    for i in hs:
        s = i.find("span")
        s.replace_with(s.string)
        i.parent.parent.replace_with(i)
print(soup.prettify())

あと、各要素にstyle="c<数字>"というstyleが入れられるのですが、これも不要なので消してしまっても良いかもしれません。

画像のSVG化

Google Documentは画像を出力してくれてよいのですが、PNGの解像度がどうしても低くなってしまいます。
そこで、Google Slideから取り込んだ画像はより高解像度なSVGに置き換えます。
まずはスライドをSVGとしてダウンロードします。
google-slide-svg-export.png
次に、ダウンロードしたHTMLのimages内にある、スライドに対応する画像のファイル名(image<数字>.png)を探しておいて、先程ダウンロードしたSVGファイルをimage<数字>.svgにリネームします。
後は、HTMLファイルの画像リンクの".png"を".svg"に書き換えます。
画像の解像度がはっきり上がるのが分かるので良いのですが、PDFとして書き出した後に、レンダリングで時間がかかるようになります。一長一短ですね…

ページ脚注の修正

Google Documentでページ脚注を入れてHTMLで出力すると、脚注の参照元に[<数字>]というテキストを持つリンクが埋め込まれ、その参照先は文書の最後に<div>要素として記載されます。一方、CSS3(というかvivliostyle)では、脚注は文書の脚注の参照元に直接<span style="float: footnote">で埋め込んでおきます。今回はCSSセレクタを使った<span class="footnote">というタグを手動で埋め込みましたが、これもスクリプトで変換できると楽そうです。

コードブロックの修正

これは手作業で直していきます。Google Documentでコードを認識する機能が無いから仕方なし。
素の文章で入力されていると<br/>の実装が<p><span>コード</span></p>とかに展開されているので悲惨です。
諦めて、まとめて<pre><code class="language-*">コード</code></pre>に変換したほうが良いです。

数式の入力

綺麗な数式は良いですよね。ということで、MathML + MathJaxを使って数式を入力します。
MathMLは流石に数式専用の処理系ということもあって、行列とかも綺麗に書けます。
ただ、MathMLを直接手書きするのはかなり辛いです… なんとかならないもんでしょうか…

CSS組版

いよいよCSSを使って組版です。vivliostyleのビューアを使ってCSSを少しずつ変えながら確認をしていきます。
vivliostyle-viewer.png
すでにいろいろと公開されている資料を見ると分かることは分かるのですが、自分がやったことをサクッとまとめておきます。

デフォルト設定

まずHTMLをvivliostyleのビューアで開いてから、紙面のサイズを設定(例えばA4など)して、その結果表示されるCSS Detailsを表示します。
vivliostyle-settings.png
次に、HTMLにインポートするCSSを作成し、そこに先程の設定をコピペします。

common.css
@page { size: A4; }

このcommon.cssをhtmlから読み込むようにすれば、次からは勝手にこの設定が適用されます。

フォント

人によると思いますが、本文は明朝体、章や節のタイトルはゴシック体というのがしっくり来ます。Webフォントを使う、ということがよくされるようですが、面倒なのでシステムにフォントをインストールしました。この方がSVGとかでフォント指定するときに楽なので良いかもしれません。
作成したのがUbuntuだったので、インストールされている「Noto Serif」「Noto Sans」に加えて「Source Code Pro」、「超極細ゴシック」を使いました。

common.css
/* 地の文は明朝体 */
:root { 
  font-size: 10.5pt; 
  font-family: "Noto Serif", "serif"; 
}
/* 文書の一番最初のタイトル */
h1.title {
  font-family: "Chogokuboso Gothic", "Noto Sans","sans-serif";
  font-weight: 1000;
  text-align: center;
  font-size: 250%;
}
/* 章タイトルはセンター寄せで大きな明朝体 */
h1 {
  font-family: "Noto Serif", "serif";
  font-weight: 800;
  text-align: center;
}
/* 節、項のタイトルはゴシック体 */
h2,h3,h4 {
  font-family: "Noto Sans", "sans-serif";
  font-weight: 700;
}
/* コードとリンクはSource Code Pro */
code, pre, a {
  font-family: "Source Code Pro", "Courier New", "monospace";
}

目次と章の構成

<nav role="doc-toc">...</nav>という構造があると、vivliostyleはその要素の子要素に含まれるリンクを辿って文書に付け加えてくれるようです。
章ごとに異なるHTMLを作成しておいて、

index.html
  <nav id="toc" role="doc-toc">
    <h2>目次</h2>
    <ul>
    <h3>
      お絵描きの国のアリス
      <author>しーげっち</author>
    </h3>
      <li><a href="doc1-chapter1.html">はじめに</a></li>
      <li><a href="doc1-chapter2.html">お絵描きソフトのプログラムアーキテクチャ</a></li>
      <li><a href="doc1-chapter3.html">お絵描きソフトを支える技術</a></li>
      ...
    </ul>
  </nav>

という内容を記述します。このindex.htmlを処理すると、自動的に全ての文書が取り込まれて1冊の本が出来てくれます。

カラーとモノクロ

CSSの設定を3つのファイルに分けます。

  1. カラー版とモノクロ版に共通の装飾設定(margin、padding、border、fontなど)
  2. カラー版に固有の設定(要素の色、模様など)
  3. モノクロ版に固有の設定(グレースケールの色、画像の白黒化など)

このなかで、共通設定の部分をcommon.cssに書き、固有の設定をそれぞれcolor.cssmono.cssに書きます。

mono.css
@media print {
  /* 画像はグレースケール化 */
  img {
    -webkit-filter: grayscale(100%);
    filter: grayscale(100%);
  }
  /* リンクは黒テキスト化 */
  a { color: black; }
}

それから、common.css

common.css
@import url(./mono.css); /* カラーの場合は color.css */

と書いておけば、適切な設定を読み込んでくれます。とても簡単です。
モノクロのときはハイコントラスト、カラーのときはいろいろな色を使う、といった使い分けが簡単に出来ます。

改ページ

いくつかやり方はあると思いますが、私は<hr>タグを使うことにしました。

common.css
hr.page-wrap {
  break-before: page;
  visibility: hidden;
  margin: 0px;
  padding: 0px;
  height: 1px;
}

としておいて、本文に

doc.html
ここまでが前のページ
<hr class="page-wrap" />
ここからが次のページ

と書くと改ページされます。

脚注

脚注を作るときには、CSSの変数を利用します。テンプレだと思うのですが、サンプルから探すのが結構疲れたので、抜き出しておきます。

doc.html
本文中に脚注<span class="footnote">これが脚注の本体</span>を書きます。

CSSには脚注本体と、脚注へのリンクを示すマーカーの両方の記述を書きます。

common.css
html { counter-reset: footnote; }
@media print {
  /* 脚注本体の書式 */
  .footnote {
    float: footnote; /* CSSでページ下部に脚注を置くための設定 */
    font-size: 8pt; /* フォントは小さくしたい */
    counter-increment: footnote;
    text-indent: 0;
  }
  /* 脚注本体の行頭に[<数字>]を挿入する */
  .footnote::footnote-marker {
    content: "[" counter(footnote) "]";
    font-size: 8pt;
    display: inline;
    vertical-align: super;
  }
  /* 脚注の参照元にリンクを作成する */
  .footnote::footnote-call {
    content: "[" counter(footnote) "]";
    font-size: 8pt;
    vertical-align: super;
    display: inline;
    line-height: 1;
  }
}

こうすると脚注は出来るのですが、index.htmlから章ごとの文章を取り込むとき、各章ごとで番号がリセットされるので通番を付けることは出来ませんでした。だれか、文書全体を通して通番で採番する方法を知っていたら教えてください…

ヘッダとフッタ

各ページの上部に文書や章のタイトル、下部にページ番号を表示、というのはよくあります。
更に、奇数ページと偶数ページでレイアウトを変えたいこともよくあります。(例えばページ番号を真ん中ではなく、ページの外側に表示させたいときなど)
vivliostyleのユーザグループで作られたサンプルを参照すると分かりやすいです。

common.css
/* 横書きの場合の左ページ */
@page:verso {
  @top-left {             /* ページヘッダ(左) */
    font-family: "Noto Sans", "sans-serif";
    content: env(pub-title); /*書名=(index.html)の<title>要素を記載*/
  }
  @bottom-left {          /* ページフッタ(左) */
    content: counter(page); /* ページ番号 */
  }
}
/* 横書きの場合の右ページ */
@page:recto {
  @top-right {             /* ページヘッダ(右) */
    font-family: "Noto Sans", "sans-serif";
    content: env(doc-title); /* このhtmlの<title>を表示 */
  }
  @bottom-right {          /* ページフッタ(右) */
    content: counter(page); /* ページ番号 */
  }
}

ソースコードのハイライト

highlight.jsとかprism.jsとかを使えばいいんだと思いますが、今回、何故か上手く出来ませんでした。HTMLをそのままブラウザで開くと反映されるんですが、vivliostyle viewerを経由すると上手く出来ませんでした。なぜ…?

その他困ったこと

vivliostyleのビューアが、突然HTMLを更新してくれなくなってしまうことがありました。しばらく放置してから読み込むと上手く読めることもあったので、何かのタイムスタンプ周りの問題が起こってるんだと思いますが…
SMBを使ってファイル共有していたりしていたので、その辺りが問題かもしれません。

電子版PDFの作成

これでいいか、と思ったらPDFを作ります。最近はvivliostyleコマンドが多機能になっているようなので、次のようにすれば良いです。

build.sh
vivliostyle build -o doc.pdf -b index.html

実行すると、裏でHeadless Chromiumが起動して、しばらくするとPDFが出来上がります。

入稿用PDFの作成

さて、これで終わらないところがCSSの組版の辛いところ…

CSS組版の光と闇を見てもらうと分かるのですが、vivliostyle(というかChromium)が生成するPDFには「Type3フォント」が大量に埋め込まれ、これが印刷所では処理できないことが多々あります。(実際、Inkscapeで読み込めなかったりします。)

上のリンクの記事ではInDesignか、MacOSのpreview.appを使う、ということになっていましたが、そんな環境は持っていません…
困った…とTwitterでつぶやいたところ、解決方法(になるはずのもの)を教えてもらいました。ありがたいことです。

なんと、GhostScriptを使うのだそうです。確かにPDFハンドリングできますよね……

pdf-outline-font.sh
DOC=$1
DOC=${DOC:-doc.pdf}
gs -dNOPAUSE -dBATCH -dNoOutputFonts -sDEVICE=pdfwrite -o outline-$DOC -f $DOC

この方法だと、フォントは全てパスとして展開されるので、テキスト情報は失われます。ただ、入稿用のPDFとしては確実なんじゃないかと思います。

もっとも、今回やむを得ない事情により技術書典8が中止になってしまいましたので、この方法が印刷書で問題なく印刷できるのか、確認は出来ていません。論理的には問題ないはずですが。

表紙の合成

入稿用のPDFなら、表紙は別ファイルで管理すれば良いかもしれません。ただ、電子版だと表紙を合成したくなります。
複数のPDFを合成してくれるプログラムは無いかな−と思っていくつか見ていましたが、それよりもPyPDF2パッケージを使って自分でスクリプトを書いたほうが早い、という結論になりました。

bookbinding.py
#!/usr/bin/env python3

import sys
import PyPDF2

output_name = sys.argv[1]  # 出力するPDFファイル名

front_cover = sys.argv[2]  # 表の表紙のPDFファイル名
inner_cover = sys.argv[3]  # 内表紙のPDFファイル名
book_body   = sys.argv[4]  # 文書本体のPDFファイル名
back_cover  = sys.argv[5]  # 裏の表紙のPDFファイル名

with open(front_cover, mode='rb') as fc, \
     open(inner_cover, mode='rb') as ic, \
     open(book_body, mode='rb') as bb, \
     open(back_cover, mode='rb') as bc:
    front_reader = PyPDF2.PdfFileReader(fc)
    inner_reader = PyPDF2.PdfFileReader(ic)
    body_reader  = PyPDF2.PdfFileReader(bb)
    back_reader  = PyPDF2.PdfFileReader(bc)
    writer       = PyPDF2.PdfFileWriter()

    fc_page = front_reader.getPage(0)
    print("Add Front Cover %s"%(front_cover))
    writer.addPage(fc_page)
    writer.addBlankPage()

    ic_page = inner_reader.getPage(0)
    print("Add Inner Cover %s"%(inner_cover))
    writer.addPage(ic_page)

    for i in range(1, body_reader.numPages):
        page = body_reader.getPage(i)
        print("Add page %d"%(i+1))
        writer.addPage(page)

    if body_reader.numPages % 2:
        writer.addBlankPage()

    bc_page = back_reader.getPage(0)
    print("Add Back Cover %s"%(back_cover))
    writer.addBlankPage()
    writer.addPage(bc_page)

    with open(output_name, mode='wb') as output:
        writer.write(output)

これで電子版のファイルが出来ました。

やってみて

CSSを知っていれば、割と簡単にページのレイアウトが出来るのは良いですね。ただ、やっぱり「CSSのmedia仕様はよく知らない…」とかでググらなければならないことは出てきます。原稿をギリギリまで書いていて、最後に組版をしようとして思わぬところで足をすくわれかねない気はしました。
一度CSS組版の基礎を確立してしまえば、2回目からはMarkdown→HTML→細かく修正でもなんとかなりそうな気はしました。ただ、図表に関して言えばWYSIWYGエディタのほうが圧倒的なので、何を重視するかで決まるのかもしれません。
CSS組版をやるときは、一度自分なりのワークフローを確立するのが大切ですね。

余談

今回、同人誌を作ってわかったのですが、このくらいの分量の文章だと、あっという間にA4数十ページの記事になってしまうんですね…
びっくりです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away