Daijiro Taira

Office・ドキュメントWindows 版を配布

PPTX結合ツール(PPTX Merger)

複数のPowerPointファイルを、決めた順番のまま1つのファイルにまとめるツールです。

Windows専用・PowerPoint Desktop必須(COM自動化のため)。Web での一般公開はしていません。

PPTX結合ツール(PPTX Merger) の操作の様子
実アプリを操作して録画:3件のPPTXを追加→順序を入れ替え→「結合する」を押すと処理状況パネルが「merging/スライドを結合中」に切り替わり、完了後にダウンロードボタンが表示されます。

exeを起動するとブラウザが自動で開き、そこにpptxファイルをドラッグ&ドロップして「上へ」「下へ」ボタンや並べ替えで順番を整え、「結合する」ボタンを押すだけで使えます。実際の処理はこのPCにインストールされているPowerPointアプリを裏で操作して行うため、Windows版PowerPoint Desktopが入っていないパソコンでは動きません。処理中はPowerPointの画面が実際に表示され、結合後は自動でファイルの中身(スライド枚数など)を確認してからダウンロードできるようになります。

使い方

  1. 結合したいPPTXを追加する

    結合したいPPTXを追加する

    ドロップゾーンにファイルをドラッグするか、クリックして複数選択します。ここでは 実験A.pptx/実験B.pptx/実験C.pptx(各27.6KB)の3件を追加し、「3 / 20件」と表示されています。1ファイル200MB・合計500MBまで、一度に追加できるのは20ファイルまでです。

  2. 上へ/下へボタンやドラッグで順番を入れ替える

    上へ/下へボタンやドラッグで順番を入れ替える

    2番目の項目(実験A.pptx)の「上へ」ボタンをクリックした直後の状態です。並びが 実験A→実験B→実験C から 実験B→実験A→実験C に変わっています。この一覧の並び順がそのまま結合後のスライド順になります(行自体のドラッグ&ドロップでの並べ替えにも対応)。

  3. 出力ファイル名を決めて「結合する」を押す

    出力ファイル名を決めて「結合する」を押す

    出力ファイル名欄を空欄のままにすると既定の merged.pptx になります。「3件を結合する」を押すとジョブが作成され、処理状況パネルが 受付済み→PowerPointへ接続中→入力を検査中→スライドを結合中→出力を検証中 の順に0.7秒間隔のポーリングで更新されます。今回は3件とも小さいファイルだったため数秒で完了まで進み、このスクリーンショットの時点ではすでに state=completed・進捗100%・ダウンロードボタン表示済みの状態になっています。

  4. 完了後、結合済みPPTXをダウンロードする

    完了後、結合済みPPTXをダウンロードする

    処理状況が completed/「完了」になると「結合済みPPTXをダウンロード」ボタンが有効になります。押すと /api/jobs/{job_id}/download からファイルが返され、この時点でアップロード用に保存していた入力ファイルのコピーと一時ファイルはサーバー側からすでに削除されています(元の入力ファイル自体は最初から一度も上書きしていません)。

ダウンロード

仕組みと使った技術

全体の流れ

  1. ブラウザ(index.html/app.js)でファイルをFormDataとして/api/mergeへPOSTすると、サーバーは拡張子・1ファイル200MB/合計500MBの容量上限・PPTX内部のZIP構造(暗号化フラグ・危険パス・マクロ混入・エントリ数上限20,000・展開後サイズ上限約1GB・CRC検証)を検証してから、job_dir/inputs/にアップロード用コピーとして保存する。
  2. ジョブはJobStoreに登録され、専用ワーカースレッド(JobWorker)がqueue.Queueから1件ずつ取り出して直列に処理する。フロントエンドは/api/jobs/{job_id}を約0.7秒間隔でポーリングし、phase(受付済み→PowerPointへ接続中→入力を検査中→スライドを結合中→出力を検証中→完了)とprogressを表示する。
  3. merge_job()はpythoncom.CoInitialize()の後、既にPowerPointが起動していればGetActiveObjectで、なければDispatchで新規起動して接続し、Visible=Trueで画面上に表示する。
  4. 各入力ファイルを読み取り専用で開いてスライド数とスライドサイズを取得し(_inspect_inputs)、新規プレゼンテーション(Presentations.Add())の既定スライドをすべて削除してから、先頭ファイルのサイズにPageSetupを合わせる。
  5. Slides.InsertFromFileを順番に呼び出して各ファイルのスライドを末尾へ追記し、挿入枚数の検証と、スライドサイズが先頭ファイルと異なる場合の警告記録を都度行う。
  6. 全件挿入後に合計スライド数を検証してから一時ファイルへSaveAs(形式24=PowerPoint 2007以降のXML形式)し、そのファイルを再度PowerPointで開き直してスライド数とサイズを再検証してから、正式な出力パスへリネームする(自己検証ステップ)。
  7. 入力コピー・一時ファイル・ジョブ用フォルダを削除して後始末し、自分で起動したPowerPointだけをQuit()する(もともと起動していたPowerPointは終了させない)。

使った技術

Python 3.9+
バックエンド全体のランタイム(pyproject.tomlでrequires-python>=3.9)
FastAPI + uvicorn
127.0.0.1:8069で待ち受けるローカル専用HTTP API。/api/merge, /api/jobs/{id}, /api/jobs/{id}/downloadを提供
pywin32(win32com.client / pythoncom)
PowerPoint COM自動化。プレゼンテーションを開く・スライド挿入・保存・再検証すべてをこの経由で実行
Vanilla HTML/CSS/JS(app/static)
ビルド不要のフロントエンド。ドラッグ&ドロップ追加、上へ/下へ並べ替え、ジョブ進捗のポーリング表示
threading.Thread + queue.Queue(app/jobs.py)
結合処理を専用ワーカースレッドで直列実行するジョブキューと進捗ステート管理
PyInstaller(--onefile)
PPTXMerger.exeへのパッケージング(scripts/pptx_merger.spec)。win32com.client等をhiddenimportsで明示

実装の工夫

  • FastAPI起動時(lifespan)にOutputフォルダとJobStore/JobWorkerを作成し、リクエストを受けるスレッドとは別のワーカースレッドで結合処理を直列実行する。複数ジョブが同時に来てもキューで1件ずつ処理されるため、同一プロセスからPowerPoint COMを同時操作してしまう競合を避けている。
  • 出力ファイル名はJobStore.reserve_output()で事前予約し、同名ファイルが既に存在すれば_2, _3…と連番を付ける。ジョブが失敗した場合はJobWorkerが予約を明示的に解放しており、同じ出力名での再試行のたびに連番が際限なくずれ続ける事故を防いでいる(jobs.pyのコメントで理由を明記)。
  • PPTXの安全性チェックはvalidation.pyがZIPを全展開せずにinfolist()だけを読み、暗号化フラグ・ZIP内の危険パス(ZIP Slip)・マクロ(vbaProject.binの存在/[Content_Types].xmlでの参照)・展開後合計サイズ上限(約1GB)・エントリ数上限(20,000)を先に弾いてからPowerPointへ渡す構成。
  • 結合後は一時ファイルを再度PowerPointで開き直し、スライド数とスライドサイズが期待値と一致するかを検証してから正式な出力パスへリネームする自己検証ステップを入れている。この経路はtests/test_com_smoke.pyで実PowerPoint・実ファイル7件72枚のE2Eテスト(結合後スライド数の一致・元ファイルのハッシュ不変・ジョブ用一時フォルダの削除まで確認)として検証済み。
  • run_local.pyはポート8069が空いているかを先に確認してからuvicornを起動し、別スレッドで/api/healthに応答があったタイミングでブラウザを自動的に開く。

用語

COM自動化(Component Object Model)
Windowsアプリが持つ機能を外部プログラムから直接操作できる仕組み。このツールはこれを使ってPowerPoint Desktopに『このファイルを開いて』『このスライドを挿入して』と直接指示している。
pywin32(win32com.client / pythoncom)
PythonからWindowsのCOMを呼び出すためのライブラリ。PowerPointへの接続・操作すべてがこの経由で行われる。
ジョブ(非同期ジョブ)
アップロード受付と実際のPowerPoint結合処理を分離し、別スレッドのワーカーが1件ずつ順番に処理する単位。フロントエンドは進捗をポーリングで確認する。
Slides.InsertFromFile
PowerPointのCOMメソッドの一つで、指定したpptxファイルのスライド一式を、開いている別のプレゼンテーションの任意の位置に挿入する。このツールの結合処理の中核。
PyInstaller onefile
Pythonの実行環境と依存ライブラリを1つのexeにまとめてパッケージする方式。実行時に中身が一時フォルダ(sys._MEIPASS)へ展開される。
ZIP Slip(危険なZIPパス)
ZIPを展開する際に「../」等のパスを使って意図しない場所にファイルを書き込ませる手口。PPTXの実体はZIPファイルなので、展開前にすべてのパスが安全かをチェックしている。