🐤

家庭用プリンターで中綴じ冊子を作るImposition(面付け)エンジンをRustで実装した【開発日誌 #8】

に公開

開発日誌 #8 です。前回はGhost Engineで常駐型デーモンアーキテクチャを作った話を書きました。

※検証環境は8年前のMacBook Airです。

今回は Publisher機能、特にImposition(面付け)エンジンの設計について書きます。
「普通のプリンターで本格的な中綴じ冊子を作る」ために、ページ順序の計算が意外と複雑でした。


Impositionとは

A4用紙に両面印刷して半分に折ると、4ページ分の冊子になります。
このとき、ページを印刷順に並べるのではなく、折ったときに正しい順序になるように並べ替える必要があります。

8ページの冊子を例にすると:

【冊子のページ順】      【印刷するときの並び】
1 2 3 4 5 6 7 8  →  表面: [8, 1] [2, 7]
                     裏面: [6, 3] [4, 5]

このページ並び替えのロジックを Imposition(面付け) と呼びます。
InDesignやAcrobatでは有料オプションですが、Hiyoko PDF VaultではRustで実装しました。


面付けのアルゴリズム

総ページ数を4の倍数に揃えた上で、各シートのページ番号を計算します。

pub fn compute_imposition(total_pages: u32) -> Vec<(u32, u32, u32, u32)> {
    // 4の倍数に切り上げ(足りない分は空白ページ)
    let padded = ((total_pages + 3) / 4) * 4;
    let sheets = padded / 4;
    let mut layout: Vec<(u32, u32, u32, u32)> = Vec::new();

    for i in 0..sheets {
        // 表面: [最終側ページ, 先頭側ページ]
        let front_right = i + 1;
        let front_left = padded - i;

        // 裏面: [先頭側ページ+1, 最終側ページ-1]
        let back_left = i + 2;
        let back_right = padded - i - 1;

        // (表左, 表右, 裏左, 裏右)
        layout.push((front_left, front_right, back_left, back_right));
    }

    layout
}

このレイアウト情報をもとに、lopdfでページを並べ替えた新しいPDFを生成します。

pub fn build_imposition_pdf(
    original: &Document,
    layout: &[(u32, u32, u32, u32)],
) -> Result {
    let mut new_doc = Document::with_version("1.5");

    for (front_left, front_right, back_left, back_right) in layout {
        // 表面シートを作成(2ページを横に並べる)
        add_sheet(&mut new_doc, original, *front_left, *front_right)?;
        // 裏面シートを作成
        add_sheet(&mut new_doc, original, *back_left, *back_right)?;
    }

    Ok(new_doc)
}

Auto TOC(自動目次)

もう一つのPublisher機能が Auto TOC(自動目次生成)です。

PDFの各ページの先頭行をヒューリスティックに解析して、見出しっぽいテキストを検出します。

pub fn detect_headings(doc: &Document) -> Vec<(u32, String)> {
    let mut headings: Vec<(u32, String)> = Vec::new();

    for (page_num, _) in doc.get_pages() {
        if let Ok(text) = doc.extract_text(&[page_num]) {
            let first_line = text.lines().next().unwrap_or("").trim();

            // 見出し判定:短くて句読点で終わらない行
            if first_line.len() > 2
                && first_line.len() < 60
                && !first_line.ends_with('。')
                && !first_line.ends_with('.')
            {
                headings.push((page_num, first_line.to_string()));
            }
        }
    }

    headings
}

検出した見出しリストをもとに、lopdfでPDFリンク付きの目次ページを先頭に挿入します。


ハマったところ

空白ページの扱い

ページ数が4の倍数でない場合、空白ページを末尾に追加する必要があります。
この空白ページのサイズを元PDFのページサイズに合わせないと、印刷時にズレが生じました。

見開きのマージン

中央で折る部分(ノド)にマージンを設けないと、文字が折り目に隠れます。
ページを並べるときに内側マージンを自動で追加する処理が必要でした。


現在の状況(dev版)


次回

次回は Sanctuary Viewer(痕跡ゼロ閲覧) の話を書きます。
PDFを開いた記録をmacOSのどこにも残さない設計です。


Hiyoko PDF Vault(日本語) → https://hiyokoko.gumroad.com/l/HiyokoPDFVault_jp
X → @hiyoyok

Discussion