Pandoc PDF出力で「1ページ目全面画像」「2ページ目標準表紙」にする方法(bxjsarticle)
Pandocを使ってMarkdownからPDF(bxjsarticle)を生成する際、「1ページ目を丸ごと全面の画像表紙(カバー)にし、2ページ目に通常の titlepage を配置したい」という状況に直面した。
Pandocの標準機能や単純なオプション指定の組み合わせでは、この仕様は満たせない。普通に画像を差し込もうとすると、LaTeXのページ生成アルゴリズムの仕様に衝突し、画像とタイトルが重なるか、あるいは1ページ目に謎の白紙が挿入されるという奇妙な挙動に悩まされることになる。
Markdownソース側を汚さず(LaTeXコードを直書きせず)、ヘッダーファイル(cover.tex)を1つ用意するだけで、このページコントロール問題をスマートに解決するシステム的なアプローチを見つけたので共有する。
解決するためのヘッダーコード(cover.tex)
まず、作業ディレクトリに cover.tex というファイルを作成し、以下のコードをそのまま貼り付ける。
\makeatletter
% 1. \maxwidth の未定義エラー対策(Pandocの仕様補完)
\(\def\maxwidth{\ifdim\Gin@nat@width>\textwidth\textwidth\else\Gin@nat@width\fi}\)
% 2. atbegshiパッケージを利用し、1ページ目の絶対座標に画像を配置
\(\usepackage{atbegshi} \AtBeginDocument{ \AtBeginShipoutNext{ \AtBeginShipoutUpperLeft{ \put(0,-\paperheight){\includegraphics[width=\paperwidth,height=\paperheight,keepaspectratio]{images_jpg/cover/cover.jpg}} } } }\)
% 3. \maketitle の挙動をフックし、標準表紙を2ページ目へ押し出す
\let\saved@maketitle\(\maketitle \renewcommand{\maketitle}{ \thispagestyle{empty} \mbox{} \%\) 空のオブジェクトを配置して1ページ目をコンテンツとして確定
\clearpage
\saved@maketitle % 2ページ目から通常のタイトルを開始
}
\makeatother
※ images_jpg/cover/cover.jpg の部分は、各自が配置している実際の画像パスに書き換えてほしい。
コンパイルコマンド
Pandocを実行する際、本文前に挿入する -B オプションではなく、-H (--include-in-header)オプションを使用して上記の cover.tex をプリアンブルに読み込ませる。
pandoc input.md -o output.pdf -H cover.tex --pdf-engine=lualatex
Markdown側のYAMLメタデータは、通常通り titlepage を指定したままで問題ない。Markdown側に不純なLaTeXコードを混ぜる必要がないため、ソースの抽象度が美しく保たれる。
--- documentclass: bxjsarticle classoption: - titlepage title: "ドキュメントタイトル" author: "著者名" --- # ここから本文 ここから通常のマークダウンのテキストを記述していく。
なぜこれで動くのか(技術的なメカニズムの分析)
LaTeXのドキュメントクラス(特に titlepage オプション有効時)は、ドキュメント開始時(\AtBeginDocument)に自動で \maketitle を呼び出して即座にタイトルページを出力しようとする。この厳格な処理順序が、前挿入しようとする画像データと衝突を起こしていた。
今回のコードが合理的に機能する理由は以下の3点にある。
atbegshiによる絶対座標配置: 画像を通常の流し込みデータとしてではなく、「1ページ目の背景レイヤー」として強制配置することで、レイアウトの崩れや意図しない余白の発生を完全に抑制している。\mbox{}による白紙化防止: LaTeXは「完全に中身が空のページ」があると自動でスキップしたり改ページ処理を狂わせたりする。そこで透明なオブジェクト(\mbox{})を置くことで、「このページにはコンテンツが存在する」とコンパイラに誤認させ、1ページ目を確定させている。\maketitleのマクロ上書き: 本来1ページ目に居座るはずの標準タイトル命令をフックし、「背景に画像を敷いた1ページ目を\clearpageで終わらせた直後に、本来のタイトル処理を実行する」という入れ子構造に書き換えている。
このアプローチにより、以下の理想的な3ページ構成が手に入る。
* 1ページ目: 全面画像(ズレなし、白紙なし)
* 2ページ目: bxjsarticle が生成する標準の titlepage
* 3ページ目: 本文スタート
PandocとLaTeXの組み合わせでページコントロールの順序にハマっている状態であれば、このコードでシステムを最適化してみてほしい。
Geminiの誤ったLaTeXコマンドを修正し、Docker+Pandocの日本語フォント設定をYAMLのみで完結させた記録
Dockerコンテナ(ベースイメージ:pandoc/latex:latest)を使い、MarkdownからLuaLaTeX経由で日本語PDFを生成する環境を構築した。
管理の観点から、フォントの指定は外部のスタイルファイルなどを使用せず、すべてMarkdownのYAML(header-includes)のみで完結させることを目指した。
その過程で、LLM(Gemini)が提示した不正確な情報と、それを修正して解決に至るまでのログを記述する。
1. 発生した問題と初期の状況
YAML側で jafont=noto(bxjsarticleクラスのプリセット)を指定し、コンテナ起動時にGoogle FontsからダウンロードしたNotoフォントを配置してPDFをビルドしたところ、以下のエラーが出力された。
! Package fontspec Error: The font "Noto Serif CJK JP Regular" cannot be found.
Google Fontsから直接ダウンロードできるのは「地域限定版(JP)」であり、LuaLaTeXのプリセットが内部で探索しているのは「汎用CJK版(CJK JP)」であるため、名前の不一致によりエラーが発生していた。
2. 検証1:AIが提案したパッケージ追加とその破綻
このエラーについてGeminiにトラブルシューティングを求めたところ、以下の回答を得た。
「Alpine Linuxがベースなので、Dockerfileに
apk add font-noto-cjkを追記してシステムにフォントをインストールすれば解決します」
指示通りにパッケージを追加して再ビルドした結果、エラー内容が以下のように変化した。
! Package fontspec Error: The font "Noto Sans CJK JP Medium" cannot be found.
原因の分析
Alpineのパッケージマネージャー(apk)でインストールされる font-noto-cjk は、容量削減のために Regular と Bold のみが含まれる最小限の構成となっている。
しかし、LuaLaTeXの jafont=noto プリセットは、ゴシック体の標準ウェイトとして Medium を厳密に探索する仕様になっている。
システム側に存在しないウェイトを要求されるため、パッケージを追加しただけでは根本的な解決にはならなかった。AIの提示したアプローチは、フォントのウェイト構成という細部を見落としていた。
3. 検証2:AIが提示したYAML直接指定とタイポの連発
プリセットの利用を諦め、手動で配置した Noto Serif JP および Noto Sans JP をYAMLの header-includes から直接マッピングする方針に切り替えた。
ここでGeminiは、YAMLに記述すべきLaTeXコマンドとして以下のコードを出力した。
header-includes: - \usepackage{luatexja-fontspec} - \setjamainfont{Noto Serif JP}
これを実行したところ、以下のエラーが発生した。
! Undefined control sequence. l.105 \setjamainfont
\setjamainfont というコマンドは存在しない(未定義)というエラーである。
このエラーを指摘すると、Geminiは「タイポでした」と謝罪し、次に以下のコードを提示した。
\setjmainfont{Noto Serif JP}
しかし、これも同様に Undefined control sequence でビルドが失敗した。LLMはLaTeXの日本語フォント関連の正確なコマンド名において、ハルシネーション(あるいは単純な文字配列の誤認)を連発する傾向がある。
4. 解決策:自力でのコマンド修正
AIが出力したコードの整合性を疑い、公式のドキュメントおよびLuaTeX-jaの仕様を確認した。
LuaLaTeX環境において、luatexja-fontspec パッケージが提供する正しい和文フォント設定コマンドは、\setmainjfont および \setsansjfont である。「j」の入る位置が異なっていた。
このファクトに基づき、MarkdownのYAMLメタデータを以下のように修正した。
documentclass: bxjsarticle classoption: - titlepage - pandoc - jafont=none # 競合を防ぐためプリセットを無効化 - fontsize=11pt header-includes: - \usepackage{luatexja-fontspec} - \setmainjfont{Noto Serif JP} # 正しいコマンド名 - \setsansjfont{Noto Sans JP} # 正しいコマンド名
この設定により、外部ファイルに依存することなく、YAMLの記述のみで意図通りの日本語フォント(Notoフォント)が適用されたPDFが正常に出力された。
5. 結論
- 技術的知見: LuaLaTeX環境で日本語フォントを直接マップする場合、
\setmainjfont/\setsansjfontを使用する。また、jafont=noneで既存のプリセット挙動を抑制する必要がある。 - LLMの利用について: 全体の構造や、エラーの背景にある仕様(フォント名の不一致など)を特定するロジックの組み立てにおいて、AIは有用な示唆を与える。しかし、ソースコードの記述、特に固有名詞やコマンドの正確性においては信頼性が低いため、人間による一次情報の検証が不可欠である。
Dockerコンテナからwebbrowser.errorを回避してflow.run_local_serverを実行する方法(Google OAuth)
エラー
上記で実行できていたflow.run_local_serverがうごかない。2023/09/27に動かなくなったことを確認。
具体的には「webbrowser.error could not locate runnable browser」とエラーが出て、ブラウザを実行できないようす。
YouTube Data APIを使うのにブラウザからのOAuth認証が必須なので、これは解消しときたい。
解決に向けた分析
エラー文言からするとどうもブラウザが実行できないというか、見つからないらしい。
実行すべきブラウザのパスがわからないらしいので明示的に指定してやればいいという情報がネット上で見つかるものの問題が。
いま、Docker上でプログラム(Pythonスクリプト)を実行しているのだ。
Dockerコンテナにはブラウザをインストールしていないから、指定すべきブラウザは存在しない。
じゃあこれまで実行できていたのがなぜなのか疑問が残る。これまではホスト側のChromeが既定ブラウザとして起動できていたのに急にできなくなったのはなぜなのか?
- Dockerの更新・仕様変更
- Pythonライブラリ(google-auth-library-python-oauthlib)の更新
- Chromeの更新
まえに実行できていたタイミング以降、それぞれ更新したかどうかもわからないが原因追及の道のりはありそう。だけど…更新履歴を探るような面倒なことは避けたい。
解決方法
幸い、OAuth認証の機会はそう頻繁じゃない。だから、
- 自動的にブラウザを起動させるのをやめる
- アクセスすべきURL文字列をDockerとHostでやり取りする
- Host側のChromeを手動で立ち上げ、OAuth認証をする
という方針でエラーを回避しよう。
方法は簡単だ。
flow.run_local_serverのキーワード引数にopen_browser=Falseを渡す
以上。
これで下記ソースコード上のエラー送出箇所を回避できる。
現況
これまで自動的に立ち上がっていたブラウザが立ち上がらなくなった。
上記対策によりVSCodeのプロンプト上に認証用のURLが表示されるようになった。
これをダブルクリックすると既定ブラウザであるChromeが立ち上がり、認証フローを進められた。
前回記事で示した通り、ホスト側からDockerへリダイレクトを通すこともできている。
Dockerコンテナへflow.run_local_serverをリダイレクトする方法(Google OAuth)
突然のエラー
2023年9月。久々にDockerコンテナ上のアプリからYouTube Data APIを触ってみると、2022年(?)までつかえていたPythonコードがエラー。
AttributeError: 'InstalledAppFlow' object has no attribute 'run_console'
run_consoleはコード実行の認可にかかわるメソッド。dir関数でしらべてみると、たしかにInstalledAppFlowオブジェクトにはrun_console属性がなく、実行できなくなった模様。
コード実行前に下記ライブラリの更新をしていたので、これが原因と判断。
- google-auth
- google-auth-httplib2
- google-auth-oauthlib
原因追及
これまで使えていたメソッドがエラーを出すだけじゃなく、それ自体がなくなってしまうというのはけっこうな異常事態な気がしたので調査。
同じような問題にぶちあたった人は海外含めているようで、いくつかの質問サイトで議論されていた。
結論としては、下記のセキュリティ強化によるものだろうと。時期的にも合致。
公式にrun_consoleは使えなくなったから存在自体なくしてしまったと。いうわけで、ライブラリのバージョンをダウングレードするとか、ソースコードをちょこっといじるといったハックは使えないと判断。
リダイレクト
run_consoleの代わりは一択で、run_local_serverを選ばざるを得ない。が、run_consoleを使っていた理由はrun_local_serverによる認可プロセスがうまくいかなかったからだ。
run_local_serverはブラウザ上での操作後、リダイレクトしてアプリにフローを引き継ぐ。しかし、Docker上のアプリではリダイレクト先の指定方法がよくわからないのだ。
この点はやはり日本でも海外でも話題になっている。どうすればうまくいくのか?と。
現時点での答えは下記にあった。
かなり息の長いIssueで、2020年に開始されて、2023年まで断続的に議論が進行している。
要点は2点。
- コンテナ起動時に-p 8080:8080のオプションをつけろ
- flow.run_local_server(bind_addr="0.0.0.0")と引数を設定しろ
以上。
すこし詳しく
-p 8080:8080
下記サイトによれば、run_local_serverは引数にportがあって、8080がデフォルトだ。
google-auth-oauthlib.readthedocs.io
なので、ポートフォワードでdockerコンテナの内と外をつないでやればいい。
もし8080ポート以外を設定したければ、引数としてport=xxxxと明示すればいい(はず)。
bind_addr="0.0.0.0"
run_local_serverの引数にbind_addr(またはhost?)を設定するとリダイレクト先が指定できる。
デフォルトが"localhost"だから、dockerを使わない場合はこれでうまくいくらしい。
dockerを使いたければ"0.0.0.0"を指定する。
イメージ
run_local_serverを実行すると、docker内にサーバーが立ち上がる。このサーバーは認可プロセスの完了を待ち受ける。
認可プロセス自体はdockerの外(ブラウザ上)で進めるから、完了後はリダイレクトでこのdocker内サーバーに伝えなければならない。
伝えるためにどこで待ち受け中かを指定したいのだが、dockerの外から内へはlocalhost("127.0.0.1")では伝わらない。
"0.0.0.0:8080"みたいに指定してやる必要がある。
ルイボスティーと乳化剤のまずい味わい
追記(2023/09)
一番のお気に入りが更新。
やさしいルイボスティーがおいしい。後発品かな?乳化剤もなくてのみやすい。リピートしてます。
ルイボスティーのみたい
ルイボスティーって変なお茶があるなと思ったところから常飲するようになったきっかけは何だったか。 たぶん、セブンイレブンで買ったペットボトルのルイボスティーがおいしかったから。いや、まぁ飲めるなと思ったから。
コーヒー・紅茶の代わりになるノンカフェイン飲料を求めていたのだ。
体質的にカフェインが利きすぎてしまう(気がする)から、常飲できるノンカフェイン飲料がほしかった。水では味がなさすぎる。できれば温めても飲みやすく、おいしければなおよし。
まぁ枯れ葉を煮出したような味がおいしいかどうかはともかく、飲める。ルイボス、飲める。
Amazonで調達
常飲するようになればいちいちコンビニでペットボトルを買うのは面倒だ。ネット通販で箱買いしちゃうほうがいい。というわけで。
あじ濃いなー。これ味濃いなーとおもいつつ、24本を消費する頃には慣れてしまった。
次に買ったのがこちら。
やすい。うまくない。なんかヘン。飲み続けることができひん。
これはいける。味うすめ。
原材料の乳化剤
それぞれの飲料の原材料を確かめてみると、気になるのは「乳化剤」。上にあげたものだと、なんかヘンな味わいがある、まずさを感じるHappy Bellyブランドの商品にだけ乳化剤が含まれる。
ペットボトル飲料は大量生産の工業製品で、乳化剤には消泡作用があったりして製造・流通に役立つことがあるらしい。だから添加するらしい。
だけど、味わい変わるらしい。たぶん。
Happy Bellyを買ったあと、いくつかのルイボスティーブランドで乳化剤が入っていることを確認し、それを避けた。避けた結果の良品物語ブランド。のみやすい。
いちおう、Happy Bellのレビューを見れば、「おいしい。のみやすい。」が並んでいることは述べておこう。
味覚は、すごく個人的なものだ。遺伝だって関係しているらしい。特定の遺伝子を持っている人だけに感じる味わいがあるらしい。たぶん、乳化剤に敏感な遺伝子もあるんやろとおもう。おいしく飲める人だってたくさんいるはず。
出会いがだいじ
ルイボスティーってあるな、試してみるかな、と思って手に取ったのがセブンイレブンのもので、たしか伊藤園が製造していて、なぜかAmazonでは流通している不思議。
不思議はさておき、これには乳化剤が入っていなかった。だから飲めた。おいしいとは言えずとも、お茶として、喫茶できた。
もしこれが乳化剤入りのもので、変な味わいをルイボスティーそのものと認識していたら、今ではルイボスティーを飲んじゃいなかったはずで。
いやほんま大事やん。出会いが、第一印象が大事やんと感じつつ、今後二度と乳化剤入りのお茶を買わないでおこうと思う。
ChromeOS/Windowsキーボードでスリープは「Win+Shift+L」
古いWindowsノートパソコンにChrome OSを導入。導入方法などは他サイトを参照のこと。
「検索」がない
Chrome OSのキーボードショートカットはChromebookの独特な「検索キー|🔍キー」を使用したものがあり、PCをスリープ状態にするショートカットもまたその一例となっている。
無操作状態で既定の時間がたてばスリープ状態へ勝手に移行するし、ノートパソコンであれば画面を閉じる動作でもスリープにできる。…がしかし、任意のタイミングで簡単にスリープにできるオプションも欲しいし実際によく使うので、やはりこれを元Windows機でも利用したい。
というわけで調べてみると、「検索キー」は「Windowsキー」に割り当てられているそうな。
Windows向けの通常の日本語タイプのキーボードなら左下に四角形と十字の窓で表現されたWindowsキーが設置されている。これがChrome OSにおける「検索」キーとしてはたらくというわけだ。
瞬息スリープ
スリープは「検索」と「Shift」と「L(エル)」の3キータイプのショートカット。ちなみにシフトキーを使わなければ、「検索」と「L」でロック(パスワード入力必須のログイン画面へ移行)となり、これはWindows機と似た挙動となっている。
よって、Windowsキーボードなら「Windows」キーと「Shift」と「L」でスリープにでき、快適な体験を得られた。
Chromebook隆盛の時代にあえて旧型のWindowsにChrome OSを導入する例は少ない気もするが、記録として残しておく。
【解決】PowerShell上のdocker runで-vオプションが利かなくなった【for Windows 2.3.0.2】
Docker for Windowsを更新。2020-05-11に公開されたDocker Desktop Community 2.3.0.2。
どうやらWindows 10 Proだけでなく、Windows 10 Homeでも導入可能になったとかでそれなりに大きなアップデートみたいです。
マウントされないボリューム
アップデート(新規インストール&旧版アンインストール?)は無事終了し、いつも通り「docker run」してみる。
動く。
ただ、いつものコマンドが通らない。
調べてみると、「-v」オプションで指定したvolumeがマウントされずホスト・コンテナ間でのファイルのやり取りができていない。
解決策
普段「docker run」コマンドは直打ちしておらず、PowerShell用のps1ファイルを読み込む形で実行していたのですが、その一部にこのような記載がありました。
-v ~/path/to/file:/file/to/path
ユーザーのホームディレクトリを示す「~」による記述。
どうやらこの記述が(PowerShellを介した「docker run」では)機能しないようになったらしく、フルパスで書けば確かにマウントされることを確認。さらに環境変数?を使用して以下のように書き換え。
-v $HOME/path/to/file:/file/to/path
この書き換えにより、アップロード前の挙動が再現されました(ただし体感では立ち上がりが数秒遅くなっているような…?)。


![[Amazonブランド] Happy Belly ルイボスティー 500ml×24本 デカフェ ノンカフェイン [Amazonブランド] Happy Belly ルイボスティー 500ml×24本 デカフェ ノンカフェイン](https://m.media-amazon.com/images/I/41xg0hfSNhL._SL500_.jpg)
![【Amazon.co.jp限定】良品物語 ルイボスティー 500ml x24本 [ポリフェノール200mg、ノンカフェイン] 【Amazon.co.jp限定】良品物語 ルイボスティー 500ml x24本 [ポリフェノール200mg、ノンカフェイン]](https://m.media-amazon.com/images/I/51xSivOwNWL._SL500_.jpg)

