生成AIでスライド画像を量産できるようになりました。
CanvaのようなスライドツールやChatGPT、Claudeで作ったものも、PNG一枚として手元に置けます。
問題は、それを「授業で使える紙」にする手間です。
画像を貼り付けて、メモ欄を作って、罫線を引いて——Wordでやると意外と時間がかかります。


そこで、スライド画像を並べるだけでワークシート形式のPDFを作るデスクトップアプリを作りました。
講師が画像を使って説明し、受講者が隣のスペースに手書きでメモできる、あの形式です。
技術的に面白かったのは、Linuxだけで開発してmacOS向けのDMGまで作れてしまったことと、TauriアプリなのにRustをほぼ書かなかったことです。
1. アプリの概要
A4縦に学習ブロックを3段並べます。
各ブロックは左側に画像、右側に手書きメモ欄という構成で、幅比率はスライダーで変えられます。
ページ数は Math.ceil(n / 3) で自動計算されます。
画像はフォルダ選択かドラッグ&ドロップで追加し、掲載順を並べ替えてPDFを書き出します。
2. Rustをほぼ書かない設計
TauriアプリはRust側にビジネスロジックを書くこともできますが、このプロジェクトではRustをTauriの起動と公式プラグインの登録だけに限定しました。
# src-tauri/Cargo.toml(ほぼこれだけ)
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"Code language: TOML, also INI (ini)
PDF生成、レイアウト計算、画像ファイル操作、掲載順管理——これらすべてをTypeScriptで実装しました。
pdf-libはブラウザとNode.jsで動くピュアJSライブラリなので、WebViewで動くTauriのフロントエンドからそのまま使えます1。
フォルダ選択には @tauri-apps/plugin-dialog、ファイル読み込みには @tauri-apps/plugin-fs を呼ぶだけで、Rustにカスタムコマンドを書かずに済みました。
2.1. UIフレームワークなしの選択
ReactやVueを入れませんでした。
HTML、CSS、DOM APIだけで3カラムの画面とドラッグ&ドロップを実装しています。
依存関係を最小にしたかったからです。
TauriのWebViewはChromiumではなくOSのWebViewを使います。
macOSはWKWebView、LinuxはWebKitGTKで、バージョンによって挙動が微妙に異なります2。
フレームワークの層が増えると、その差異が表面化しやすくなります。
状態管理はシンプルなモジュール変数で十分でした。AppState を一か所で持ち、変更のたびにDOMを再描画します。
Vitestでロジックをテストし、DOMはブラウザで確認する分離ができれば、フレームワークなしでも管理できます。
2.2. pdf-libでA4レイアウトを座標計算する
PDF座標系は左下が原点で、Y軸が上向きです3。
画面のCSSとは逆なので変換が必要になります。

// layout.ts
export const A4 = {
width: 595.28,
height: 841.89,
} as const;
const PAGE_MARGIN_X = 22;
const CONTENT_TOP = 73;
const BLOCK_HEIGHT = 198;
const BLOCK_GAP = 34;Code language: TypeScript (typescript)
単位はポイントで、1インチ72ptです。
A4の幅595.28ptは約210mmに対応します4。
ブロック座標は画面と同じ左上原点で計算して、描画時にY軸を反転しています。
// generator.ts — 描画時にY軸を反転
function drawTopRect(page: PDFPage, rect: Rect, borderWidth: number): void {
page.drawRectangle({
x: rect.x,
y: A4.height - rect.y - rect.height, // ← 変換
width: rect.width,
height: rect.height,
borderWidth,
borderColor: BLACK,
});
}Code language: TypeScript (typescript)
画像のcontain配置は fitContain 関数で計算します。
縦横比を維持したまま枠内に収め、余白を均等に取ります。
export function fitContain(
sourceWidth: number,
sourceHeight: number,
target: Rect,
): Rect {
const scale = Math.min(
target.width / sourceWidth,
target.height / sourceHeight,
);
const width = sourceWidth * scale;
const height = sourceHeight * scale;
return {
x: target.x + (target.width - width) / 2,
y: target.y + (target.height - height) / 2,
width,
height,
};
}Code language: TypeScript (typescript)
Math.min で縦横どちらが制約になるかを判断し、中央に配置します。
CSSの object-fit: contain と同じロジックを手で書いた形です5。
メモ欄の罫線5本は、枠の高さを6等分して1〜5番目の位置に引きます。
const noteRules = Array.from({ length: 5 }, (_, ruleIndex): Line => {
const ruleY = notes.y + (notes.height * (ruleIndex + 1)) / 6;
return {
start: { x: notes.x, y: ruleY },
end: { x: notes.x + notes.width, y: ruleY },
};
});Code language: TypeScript (typescript)
2.3. 日本語フォントをPDFに埋め込む
PDFにテキストを描くとき、フォントを埋め込まないと閲覧環境によって文字化けします。
日本語は代替フォントでの表示が壊れやすいため、Noto Sans JPをアセットとして同梱し、fontkit経由で埋め込みました6。
import fontkit from "@pdf-lib/fontkit";
const document = await PDFDocument.create();
document.registerFontkit(fontkit);
const font = await document.embedFont(fontBytes, { subset: false });Code language: TypeScript (typescript)
subset: false にするとフォント全体を埋め込みます7。
ファイルサイズは増えますが、タイトル文字列が何であっても確実に表示できます。
サブセット化するには使用文字を事前に確定させる必要があるため、ユーザーが任意のタイトルを入力できるこのアプリでは全体埋め込みを選びました。
2.4. 大量フォルダへの遅延読み込み
フォルダ選択時に画像本体を全件読み込むと、数百枚のフォルダで待ち時間が生じます。
フォルダ選択ではファイル情報だけを取得し、画像バイト列は表示が必要になった時点で読む設計にしました。
// フォルダ選択時はメタデータだけ
export async function loadImagesFromDirectory(
directory: string,
): Promise<SlideImage[]> {
const entries = await readDir(directory);
const paths = entries
.filter((entry) => entry.isFile && isSupportedImage(entry.name))
.map((entry) => join(directory, entry.name));
return Promise.all(paths.map(loadImageMetadata)); // stat()だけ
}
// 表示するときにバイト列を読む
export async function loadImage(
image: SlideImage,
): Promise<LoadedSlideImage> {
return {
...image,
bytes: await readFile(image.path), // ← ここで初めて読む
};
}Code language: TypeScript (typescript)
一覧は10枚単位でページ表示し、表示中の10枚だけを読みます。
作成日時を取得できないファイルは一覧の末尾に寄せ、そのなかではファイル名の数値順でソートしています。
3. LinuxだけでmacOS向けDMGを作る
ここが今回いちばん楽だった部分です。


Tauri公式のGitHub Actionsアクションを使うと、各OS向けのビルドをそのOSのランナーで自動実行できます8。
開発はLinuxだけで行い、GitHub ActionsのmacOSランナーがdmgを作ってGitHub Releasesへ置く仕組みです。
# .github/workflows/release.yml(抜粋)
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- uses: tauri-apps/tauri-action@v0
with:
args: --target ${{ matrix.target }}Code language: YAML (yaml)
Apple SiliconとIntel Mac向けにターゲットを分けてビルドします9。
ローカルのmacOS環境がなくても、PRをpushするとCIが両方の.dmgを作ってくれます。
コード署名と公証は今回対応していないため、macOSで初回起動時に「開発元を確認できない」という警告が出ます。
Gatekeeperを一時的に許可して開けば動きますが、署名なしのDMGの配布は教育用途や開発者向けに限った方が無難です10。


4. まとめ
TauriでピュアなTypeScriptアプリを作るとき、Rustに触れる必要はほぼありません。
pdf-libのようなピュアJSライブラリとTauriのプラグインだけで、ファイル操作からPDF生成まで完結できます。
macOS向けのビルドはGitHub Actionsに任せることで、Linux開発環境のまま両プラットフォームをカバーできました。
デスクトップアプリのクロスコンパイルはセットアップが大変ですが、CIがその部分を引き受けてくれます。
ソースコードと配布物は GitHub Releases から取得できます。
- TypeScriptで書かれており、ネイティブ依存なしの純粋なJavaScriptにコンパイルされています。ブラウザ、Node、Deno、React Nativeなど、あらゆるJavaScript実行環境で動作します。 – PDF-LIB
- TauriはWRYというライブラリ経由でシステムのWebViewを使います。macOS・iOSではWKWebView、WindowsではWebView2、LinuxではWebKitGTK、AndroidではAndroid System WebViewを利用します。 – Webview Versions | Tauri
- PDFの座標系はPostScriptに由来し、ページ左下が原点(0, 0)でY軸が上を向きます。HTMLやCSSが左上を原点としY軸を下向きに取るのとは逆になります。 – Create and modify PDF documents in any JavaScript environment – pdf-lib
- A4はISO 216で210mm×297mmと定義されています。1インチ=25.4mmかつ1インチ=72ptの換算から、幅は210÷25.4×72≈595.28pt、高さは297÷25.4×72≈841.89ptになります。 – pdf-lib – npm
object-fit: containは、置換要素のコンテンツが縦横比を維持しながらコンテナ内に収まるよう、必要に応じて縮小します。 – object-fit – CSS | MDN- Noto Sans JPはSIL Open Font License 1.1のもとで公開されており、アプリへの同梱や再配布が認められています。 – Use Noto fonts – notofonts.github.io
- fontkitはフォントのサブセット化、つまり指定したグリフのみを含む新しいフォントを生成する機能を持ちます。ファイルサイズ削減に有効ですが、使用文字を事前に確定させる必要があります。 – @pdf-lib/fontkit – npm
tauri-apps/tauri-actionはTauriアプリをmacOS、Linux、Windows向けにビルドしてGitHub Releasesへアップロードするまでを自動化するGitHub Actionです。 – GitHub | Tauriaarch64-apple-darwinはApple Silicon (M1以降)、x86_64-apple-darwinはIntel Mac向けのRustターゲットトリプルです。ユニバーサルバイナリとしてまとめることもできますが、ファイルサイズが倍増するため、別々のDMGとして配布する方法が一般的です。 – Ship Your Tauri v2 App Like a Pro: GitHub Actions and Release Automation- GatekeeperはmacOS Mountain Lion以降に搭載されたセキュリティ機能で、署名証明書のないアプリをブロックします。Controlキーを押しながらアイコンをクリックして「開く」を選ぶと、初回のみ許可できます。 – Mac でアプリを安全に開く – Apple サポート