【Python】自作でできる!一括PDF画像変換ツール(GUI対応・高機能)のコード紹介

Webツールを使わずに安全に!PDF→画像変換の自動化ツールをPythonで作成する python
本サイトにはプロモーションが含まれています。

 「PDFを画像(PNGやJPEG)に変換したいけれど、Webサイトにアップロードするのはセキュリティ的に不安…」「大量のファイルを一つずつ変換するのが大変…」
そんな悩みを持つ方へ。

Webツールを使わずに安全に!PDF→画像変換の自動化ツールをPythonで作成する
Webツールを使わずに安全に!PDF→画像変換の自動化ツールをPythonで作成する

 今回は、Pythonを使って「自分のPC内で完結する、高機能なPDF画像変換ツール」を紹介します。

 単に変換するだけでなく、解像度の調整や、ファイルごとの保存先指定など、実用性を追求したコードのポイントを解説します。

なぜこのPDF画像変換ツールを作成したか

 生命保険の保険金をネットで請求する際に、スキャンした診療明細書(PDF)を他の画像(PNGなど)に変換する必要があったので、AI先生に助言をいただきつつ「PDF画像変換ツール」を作成しました。

PDF画像変換ツールの主な特徴

  1. 直感的なGUI操作
    • コマンドを打ち込む必要はありません。Tkinterを使用した専用ウィンドウで、ボタン操作とファイル選択だけで完結します。
  2. マルチファイルの一括処理
    • 複数のPDFファイルを一度に選択し、順番に自動で変換処理を進めます。
  3. 柔軟な出力設定
    • 保存形式: PNG, JPEG, BMP, TIFF から選択可能。
    • 解像度(DPI)調整: 72-600 DPIまで自由に変更でき、印刷用からWeb用まで対応。
  4. 個別保存先フォルダの指定
    • ファイルごとに「どこに保存するか」をその都度確認できるため、整理整然とフォルダ分けして保存できます。
  5. マルチスレッド対応(フリーズ防止)
    • 変換中にアプリが「応答なし」になるのを防ぐため、バックグラウンド処理(Threading)を実装しています。

PDF画像変換ツールを使うメリット

1. セキュリティとプライバシーの確保

 オンラインの無料変換サイトを利用すると、自分のPDFデータが外部サーバーにアップロードされることになります。このツールは完全にローカル環境(自分のPC内)で動作するため、機密性の高い書類も安心して変換できます。

2. 高品質なカスタマイズ

 多くの無料ツールでは解像度が固定されていますが、このツールならDPIを指定して高品質な画像を取得できます。また、透過処理(RGBAからRGBへの変換)も組み込まれているため、JPEG保存時のエラーも回避できています。

3. 効率的なワークフロー

 「1ファイルごとにフォルダを新設して保存する」という作業を自動化できます。大量の資料をプロジェクトごとに整理して保存したい事務作業や、クリエイティブな制作現場で大きな威力を発揮します。

【Pythonコード】PDF画像変換ツール

import sys
import os
import threading
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from pdf2image import convert_from_path
from PIL import Image

# ==========================================
# 【重要】ダウンロードしたpopplerのbinフォルダのパスを指定してください
# ==========================================
# .exe化された際の一時解凍先パス、または通常実行時のパスを自動判定
if getattr(sys, 'frozen', False):
    base_path = sys._MEIPASS
else:
    base_path = r"C:\pdf_converter" # ご自身の環境に合わせて書き換えて下さい。

POPPLER_PATH = os.path.join(base_path, "poppler-26.02.0", "Library", "bin")

class PDFConverterApp:
    def __init__(self, root):
        self.root = root
        self.root.title("一括PDF画像変換ツール (個別保存先確認)")
        self.root.geometry("500x300")
        
        self.pdf_paths = []
        self.current_file_idx = 0
        self.total_saved_images = 0
        
        self.create_widgets()

    def create_widgets(self):
        # メインフレーム
        frame = ttk.Frame(self.root, padding=20)
        frame.pack(fill=tk.BOTH, expand=True)

        # ファイル選択状況ラベル
        self.label_info = ttk.Label(
            frame, 
            text="PDFファイルを選択してください。(複数選択可)", 
            anchor="center", 
            font=("MS Gothic", 10, "bold")
        )
        self.label_info.pack(pady=15)

        # ファイル選択ボタン
        self.btn_select = ttk.Button(frame, text="PDFファイルを選択", command=self.select_pdfs)
        self.btn_select.pack(fill=tk.X, pady=5)

        # 設定エリア(横並び)
        settings_frame = ttk.Frame(frame)
        settings_frame.pack(pady=15)

        # 保存形式
        ttk.Label(settings_frame, text="保存形式:").grid(row=0, column=0, padx=5)
        self.combo_format = ttk.Combobox(settings_frame, values=["PNG", "JPEG", "BMP", "TIFF"], width=8, state="readonly")
        self.combo_format.set("PNG")
        self.combo_format.grid(row=0, column=1, padx=10)

        # 解像度 (DPI)
        ttk.Label(settings_frame, text="解像度 (DPI):").grid(row=0, column=2, padx=5)
        self.spin_dpi = tk.Spinbox(settings_frame, from_=72, to=600, width=5)
        self.spin_dpi.delete(0, "end")
        self.spin_dpi.insert(0, "150")
        self.spin_dpi.grid(row=0, column=3, padx=10)

        # 変換実行ボタン
        self.btn_convert = ttk.Button(frame, text="画像への変換を開始", command=self.start_batch_conversion, state=tk.DISABLED)
        self.btn_convert.pack(fill=tk.X, pady=5)

        # ステータス表示ラベル
        self.label_status = ttk.Label(frame, text="", anchor="center", foreground="blue", font=("MS Gothic", 10, "bold"))
        self.label_status.pack(pady=15)

    def select_pdfs(self):
        # 複数選択可能
        file_paths = filedialog.askopenfilenames(
            title="PDFファイルを開く",
            filetypes=[("PDF Files", "*.pdf")]
        )
        if file_paths:
            self.pdf_paths = list(file_paths)
            self.label_info.config(text=f"選択中: {len(file_paths)} 個のファイル")
            self.btn_convert.config(state=tk.NORMAL)
            self.label_status.config(text="")

    def start_batch_conversion(self):
        self.current_file_idx = 0
        self.total_saved_images = 0
        
        # 画面の入力欄を一時的に無効化
        self.btn_select.config(state=tk.DISABLED)
        self.btn_convert.config(state=tk.DISABLED)
        self.combo_format.config(state=tk.DISABLED)
        self.spin_dpi.config(state=tk.DISABLED)
        
        # 最初のファイル処理へ
        self.process_next_file()

    def process_next_file(self):
        # すべてのファイルが終了した場合
        if self.current_file_idx >= len(self.pdf_paths):
            self.label_status.config(text=f"すべて完了: 計 {self.total_saved_images} 枚の画像を保存しました。")
            self.btn_select.config(state=tk.NORMAL)
            self.btn_convert.config(state=tk.NORMAL)
            self.combo_format.config(state=tk.NORMAL)
            self.spin_dpi.config(state=tk.NORMAL)
            messagebox.showinfo("完了", f"すべてのPDFの変換が完了しました!\n計 {self.total_saved_images} 枚保存されました。")
            return

        current_pdf = self.pdf_paths[self.current_file_idx]
        base_name = os.path.basename(current_pdf)
        file_info = f"[{self.current_file_idx + 1}/{len(self.pdf_paths)}]"

        # ダイアログを最前面に出すために一度更新
        self.root.update()
        
        # 個別保存先確認ダイアログ
        output_dir = filedialog.askdirectory(
            title=f"{file_info}「{base_name}」の画像保存先フォルダを選択してください"
        )
        
        # キャンセルされた場合は次のファイルへ
        if not output_dir:
            self.current_file_idx += 1
            self.process_next_file()
            return

        img_format = self.combo_format.get()
        try:
            dpi = int(self.spin_dpi.get())
        except ValueError:
            dpi = 150

        # フリーズ防止のため、実際の変換処理はバックグラウンド(別スレッド)で実行
        threading.Thread(
            target=self.convert_worker, 
            args=(current_pdf, output_dir, img_format, dpi, file_info),
            daemon=True
        ).start()

    def convert_worker(self, pdf_path, output_dir, img_format, dpi, file_info):
        try:
            base_name = os.path.splitext(os.path.basename(pdf_path))[0]
            self.update_status_ui(f"{file_info} PDFを解析中: {base_name}.pdf")
            
            images = convert_from_path(pdf_path, dpi=dpi, poppler_path=POPPLER_PATH)
            total_pages = len(images)
            ext = img_format.lower()

            for p_idx, image in enumerate(images):
                page_num = p_idx + 1
                self.update_status_ui(f"{file_info} 保存中... {page_num}/{total_pages}ページ ({base_name})")
                
                out_path = os.path.join(output_dir, f"{base_name}_{page_num:03d}.{ext}")
                
                if img_format in ["JPEG", "BMP"] and image.mode in ('RGBA', 'LA'):
                    background = Image.new('RGB', image.size, (255, 255, 255))
                    background.paste(image, mask=image.split()[-1])
                    background.save(out_path, img_format)
                else:
                    image.save(out_path, img_format)
                
                self.total_saved_images += 1
            
            # このファイルの処理が終わったらメインスレッド経由で次のファイルへ
            self.root.after(0, self.on_file_finished)

        except Exception as e:
            self.root.after(0, lambda: messagebox.showerror("エラー", f"変換中にエラーが発生しました:\n{str(e)}"))
            self.root.after(0, self.on_file_finished)

    def update_status_ui(self, text):
        # スレッドから安全にUIを書き換える
        self.root.after(0, lambda: self.label_status.config(text=text))

    def on_file_finished(self):
        self.current_file_idx += 1
        self.process_next_file()

if __name__ == "__main__":
    root = tk.Tk()
    app = PDFConverterApp(root)
    root.mainloop()