officeファイルをドラッグアンドドロップでPDF化するツール作ってみた

# はじめに
社内で毎月大量のExcel・WordファイルをPDF化することってありますよね。
毎回ファイルを開いて、エクスポート・・・
１つだけならまだしも、毎回何十ものファイルをいちいち開いてPDFにするなんて**賽の河原の石積み**くらいしんどいです。
実際去年自分もしんどいなーと思ったので、Pythonでサクッとドラッグアンドドロップ操作でPDFにするツールを作成しました。

## 目的
・Officeファイル（Excel、Word、powerpointのファイル）をドラッグアンドドロップでPDF変換
・複数ファイルのドラッグアンドドロップに対応
・フォルダのドラッグアンドドロップにも対応

## 構築環境
・Windows
・vscode
・Python3.13
本ページに記載するコードを実行する際は、Python環境の構築が必要にはなってきます。
ただし、ただ業務を効率化したい方のために環境構築不要のExeファイルも最後に用意しておりますので、安心してご覧ください。（~~いちいち業務用PCに環境構築してられないですもんね~~）

## 実装コード
### ファイル直下出力版
こちらは、**ドラッグアンドドロップしたファイルが格納されているフォルダ内**にPDFを生成するコードです。
フォルダをドラッグアンドドロップすると、フォルダ内のOfficeファイル全てだけでなく、フォルダ内のフォルダ内OfficeファイルまでPDF化するので、PDFファイル格納場所を元のOfficeファイルと同じフォルダ階層で設定したい場合に使えます。
```Python:PDF_converter.py
import xlwings as xw
from tkinterdnd2 import TkinterDnD, DND_FILES
import tkinter as tk
import os
from win32com import client

def drop_event(event):
    # ドラッグアンドドロップされたデータを取得
    raw_data = event.data.strip()

    # データを分割しない状態で処理
    file_paths = parse_dropped_paths(raw_data)

    for file_path in file_paths:
        # ファイルパスの前後に{}があれば削除
        if file_path.startswith("{") and file_path.endswith("}"):
            file_path = file_path[1:-1]

        if os.path.exists(file_path):
            if os.path.isfile(file_path) and file_path.lower().endswith((".xls", ".xlsx", ".xlsm",".csv")):
                # Excelファイルを直接処理
                process_excel_file(file_path)
            elif os.path.isdir(file_path):
                # フォルダ内のすべてのExcelファイルを処理
                for root_dir, _, files in os.walk(file_path):
                    for file in files:
                        if file.lower().endswith((".xls", ".xlsx", ".xlsm")):
                            process_excel_file(os.path.join(root_dir, file))

            if os.path.isfile(file_path) and file_path.lower().endswith((".docx", ".doc")):
                # Wordファイルを直接処理
                process_word_file(file_path)
            elif os.path.isdir(file_path):
                # フォルダ内のすべてのWordファイルを処理
                for root_dir, _, files in os.walk(file_path):
                    for file in files:
                        if file.lower().endswith((".docx", ".doc")):
                            process_word_file(os.path.join(root_dir, file))

            if os.path.isfile(file_path) and file_path.lower().endswith((".pptx", ".pptm",".ppt")):
                # powerpointファイルを直接処理
                process_pptx_file(file_path)
            elif os.path.isdir(file_path):
                # フォルダ内のすべてのpowerpointファイルを処理
                for root_dir, _, files in os.walk(file_path):
                    for file in files:
                        if file.lower().endswith((".pptx", ".pptm",".ppt")):
                            process_pptx_file(os.path.join(root_dir, file))
        else:
            print(f"無効なパス: {file_path}")

    print("すべての処理が完了しました。")

def parse_dropped_paths(raw_data):
    """
    ドラッグ＆ドロップされたデータを正しく分割してリストにする。
    スペースを含むパスも正しく処理する。
    """
    paths = []
    temp_path = ""
    in_quotes = False

    for char in raw_data:
        if char == '"':  # 引用符をトグル
            in_quotes = not in_quotes
            if not in_quotes and temp_path:
                paths.append(temp_path.strip())
                temp_path = ""
        elif char == ' ' and not in_quotes:  # 引用符外のスペースでパス終了
            if temp_path:
                paths.append(temp_path.strip())
                temp_path = ""
        else:
            temp_path += char

    if temp_path:  # 最後のパスを追加
        paths.append(temp_path.strip())

    return paths

def process_excel_file(file_path):
    try:
        # ファイル名をリストボックスに表示
        file_name = os.path.basename(file_path)
        file_listbox.insert(tk.END, file_name)

        # ExcelファイルをPDFに変換
        convert_workbook_to_pdf(file_path)
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

def convert_workbook_to_pdf(file_path):
    try:
        # Excelアプリケーションを非表示で起動
        app = xw.App(visible=False)

        # ワークブックを開く
        wb = app.books.open(file_path)

        # PDF保存先のパスを設定
        output_dir = os.path.dirname(file_path)
        base_name = os.path.splitext(os.path.basename(file_path))[0]
        pdf_path = os.path.join(output_dir, f"{base_name}.pdf")

        # ワークブック全体をPDFとしてエクスポート
        wb.api.ExportAsFixedFormat(0, pdf_path)

        # ワークブックを閉じる
        wb.close()

        print(f"PDFに変換しました: {pdf_path}")
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)
    finally:
        # Excelアプリケーションを終了
        app.quit()

def process_word_file(file_path):
    try:
        # ファイル名をリストボックスに表示
        file_name = os.path.basename(file_path)
        file_listbox.insert(tk.END, file_name)

        # WordファイルをPDFに変換
        pdf_path = convert_word_to_pdf(file_path)

    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

def convert_word_to_pdf(file_path):
    try:
        # Wordアプリケーションを起動
        word = client.Dispatch("Word.Application")
        doc = word.Documents.Open(file_path)

        # PDF保存先のパスを設定
        output_dir = os.path.dirname(file_path)
        base_name = os.path.splitext(os.path.basename(file_path))[0]
        pdf_path = os.path.join(output_dir, f"{base_name}.pdf")

        # PDF形式で保存
        doc.SaveAs(pdf_path, FileFormat=17)  # 17はPDFのファイル形式

        print(f"PDFに変換しました: {pdf_path}")

        # ドキュメントを閉じる
        doc.Close()
        word.Quit()

        return pdf_path
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)
        return None

def process_pptx_file(file_path):
    try:
        # ファイル名をリストボックスに表示
        file_name = os.path.basename(file_path)
        file_listbox.insert(tk.END, file_name)

        # WordファイルをPDFに変換
        pdf_path = convert_pptx_to_pdf(file_path)

    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

def convert_pptx_to_pdf(file_path):
    try:
        # PowerPointアプリケーションを起動
        powerpoint = client.Dispatch("PowerPoint.Application")
        presentation = powerpoint.Presentations.Open(file_path, WithWindow=False)

        # PDF保存先のパスを設定
        output_dir = os.path.dirname(file_path)
        base_name = os.path.splitext(os.path.basename(file_path))[0]
        pdf_path = os.path.join(output_dir, f"{base_name}.pdf")

        # PDF形式で保存
        presentation.SaveAs(pdf_path, FileFormat=32)  # 32はPDFのファイル形式

        print(f"PDFに変換しました: {pdf_path}")

        # プレゼンテーションを閉じる
        presentation.Close()
        powerpoint.Quit()

        return pdf_path
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)
        return None

if __name__ == "__main__":
    # TkinterDnDウィンドウの作成
    root = TkinterDnD.Tk()
    root.title("Excel & Word to PDF Converter")
    root.geometry("400x300")

    # ラベルの作成
    label = tk.Label(root, text="ここにExcel・Word・pptxファイル、またはフォルダをドラッグ＆ドロップしてください", wraplength=300)
    label.pack(pady=10)

    # ファイル名表示用のリストボックスを作成
    file_listbox = tk.Listbox(root, width=50, height=15)
    file_listbox.pack(pady=10)

    # ドロップ操作の登録
    root.drop_target_register(DND_FILES)
    root.dnd_bind("<<Drop>>", drop_event)

    # Tkinterメインループ
    root.mainloop()

```
#### 使い方
コードを実行したら、以下のようなウィンドウが出るのでPDF化したいファイルまたは、格納フォルダをドラッグアンドドロップしてください。
数秒停止した後、プログレスバーが表示され、PDFが生成されます。
生成元のファイル名は、ウィンドウ内の白いエリア内に全て表示されます。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4071315/808b6259-b5b5-492a-afc8-b58efc85e175.png">

### 指定フォルダ内出力版
こちらは、指定したフォルダ内にPDFを生成するコードです。

```Python:PDF_converter2.py
import xlwings as xw
from tkinterdnd2 import TkinterDnD, DND_FILES
import tkinter as tk
from tkinter import filedialog, messagebox
import os
from win32com import client

# 保存先フォルダの初期値
default_output_dir = os.getcwd()

# 保存先フォルダを設定する関数
def select_output_directory():
    global default_output_dir
    selected_dir = filedialog.askdirectory(title="保存先フォルダを選択")
    if selected_dir:
        default_output_dir = selected_dir
        output_dir_label.config(text=f"保存先: {default_output_dir}")

# ドラッグアンドドロップイベント
def drop_event(event):
    raw_data = event.data.strip()
    file_paths = parse_dropped_paths(raw_data)

    for file_path in file_paths:
        file_path = file_path.strip()

        if os.path.exists(file_path):
            if os.path.isfile(file_path):
                if file_path.lower().endswith((".xls", ".xlsx", ".xlsm")):
                    process_excel_file(file_path)
                elif file_path.lower().endswith((".docx", ".doc")):
                    process_word_file(file_path)
                elif file_path.lower().endswith((".pptx", ".pptm", ".ppt")):
                    process_pptx_file(file_path)

            elif os.path.isdir(file_path):
                for root_dir, _, files in os.walk(file_path):
                    for file in files:
                        full_path = os.path.join(root_dir, file)
                        if file.lower().endswith((".xls", ".xlsx", ".xlsm")):
                            process_excel_file(full_path)
                        elif file.lower().endswith((".docx", ".doc")):
                            process_word_file(full_path)
                        elif file.lower().endswith((".pptx", ".pptm", ".ppt")):
                            process_pptx_file(full_path)
        else:
            print(f"無効なパス: {file_path}")

    print("すべての処理が完了しました。")


# ファイルパスの分割
def parse_dropped_paths(raw_data):
    paths = []
    temp_path = ""
    in_quotes = False

    i = 0
    while i < len(raw_data):
        char = raw_data[i]

        if char == '"':  # ダブルクォートの処理
            in_quotes = not in_quotes
            if not in_quotes:
                paths.append(temp_path.strip())
                temp_path = ""
        elif char == ' ' and not in_quotes:  # クォートの外でのスペースは区切りとして扱う
            if temp_path:
                paths.append(temp_path.strip())
                temp_path = ""
        elif char == '{':  # {} で囲まれたパスの処理
            end_idx = raw_data.find('}', i)
            if end_idx != -1:
                paths.append(raw_data[i + 1:end_idx].strip())
                i = end_idx  # } の位置までスキップ
            else:
                temp_path += char
        else:
            temp_path += char

        i += 1

    if temp_path:
        paths.append(temp_path.strip())

    return paths

# Excelファイル処理
def process_excel_file(file_path):
    try:
        file_name = os.path.basename(file_path)
        file_listbox.insert(tk.END, file_name)
        convert_workbook_to_pdf(file_path)
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

# ExcelファイルをPDFに変換
def convert_workbook_to_pdf(file_path):
    try:
        app = xw.App(visible=False)
        wb = app.books.open(file_path)

        base_name = os.path.splitext(os.path.basename(file_path))[0]
        pdf_path = os.path.join(default_output_dir, f"{base_name}.pdf")

        wb.api.ExportAsFixedFormat(0, pdf_path)
        wb.close()

        print(f"PDFに変換しました: {pdf_path}")
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)
    finally:
        app.quit()

# Wordファイル処理
def process_word_file(file_path):
    try:
        file_name = os.path.basename(file_path)
        file_listbox.insert(tk.END, file_name)
        convert_word_to_pdf(file_path)
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

# WordファイルをPDFに変換
def convert_word_to_pdf(file_path):
    try:
        word = client.Dispatch("Word.Application")
        doc = word.Documents.Open(file_path)

        base_name = os.path.splitext(os.path.basename(file_path))[0]
        pdf_path = os.path.join(default_output_dir, f"{base_name}.pdf")

        doc.SaveAs(pdf_path, FileFormat=17)
        doc.Close()
        word.Quit()

        print(f"PDFに変換しました: {pdf_path}")
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

# PowerPointファイル処理
def process_pptx_file(file_path):
    try:
        file_name = os.path.basename(file_path)
        file_listbox.insert(tk.END, file_name)
        convert_pptx_to_pdf(file_path)
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

# PowerPointファイルをPDFに変換
def convert_pptx_to_pdf(file_path):
    try:
        # PowerPointアプリケーションを起動
        powerpoint = client.Dispatch("PowerPoint.Application")
        presentation = powerpoint.Presentations.Open(file_path, WithWindow=False)

        # PDF保存先のパスを設定
        output_dir = default_output_dir
        base_name = os.path.splitext(os.path.basename(file_path))[0]
        pdf_path = os.path.join(output_dir, f"{base_name}.pdf")

        # PDF形式で保存
        presentation.SaveAs(pdf_path, FileFormat=32)  # 32はPDFのファイル形式

        print(f"PDFに変換しました: {pdf_path}")

        # プレゼンテーションを閉じる
        presentation.Close()
        powerpoint.Quit()
    except Exception as e:
        print(f"エラーが発生しました: {file_path}")
        print(e)

# メインウィンドウ
if __name__ == "__main__":
    root = TkinterDnD.Tk()
    root.title("ファイルをPDFに変換")
    root.geometry("400x400")

    # ラベル
    label = tk.Label(root, text="ファイルやフォルダをドラッグ＆ドロップしてください", wraplength=300)
    label.pack(pady=10)

    # 保存先フォルダの設定ボタン
    select_folder_button = tk.Button(root, text="保存先フォルダを選択", command=select_output_directory)
    select_folder_button.pack(pady=5)

    # 保存先フォルダ表示
    output_dir_label = tk.Label(root, text=f"保存先: {default_output_dir}", wraplength=300)
    output_dir_label.pack(pady=5)

    # ファイルリスト表示
    file_listbox = tk.Listbox(root, width=50, height=15)
    file_listbox.pack(pady=10)

    # ドロップイベントの登録
    root.drop_target_register(DND_FILES)
    root.dnd_bind("<<Drop>>", drop_event)

    # メインループ
    root.mainloop()
```

#### 使い方
コードを実行したら、以下のようなウィンドウが出るのでPDFの保存先フォルダを「**保存先フォルダを選択**」ボタンを押して指定してください。
PDF化したいファイルまたは、格納フォルダを白枠内にドラッグアンドドロップしてください。
数秒停止した後、プログレスバーが表示され、**指定フォルダ内にPDFが生成**されます。
生成元のファイル名は、ウィンドウ内の白いエリア内に全て表示されます。
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4071315/ec690cbc-5162-4d93-b13f-383e30ff3484.png">

## 注意
:::note warn
PDF化できるのは、拡張子が以下のファイルのみです。
Excel系：xlsx、csv、xlsm
Word系：doc、docx
PowerPoint系：pptx、pptm、ppt
:::
:::note warn
PDF化したいファイルに対応するExcel、Word、powerpointのいずれかがPCにインストールされていないと起動しない可能性があります。
例：.xlsmファイルをPDF化→Excelインストール
　　.pptファイルをPDF化→PowerPointインストール
:::
:::note warn
また、コードを実行する前にターミナル上で、
```Python
pip install xlwings, tkinterdnd2 
```
上記の2点のモジュールをインストールしてください。
:::

## まとめ
これがあれば、印刷時のビューと全く同じ表示形式でPDFが生成されます。
社内の定例作業をかなり効率化できるので、ぜひ使ってみてください。

# おまけ
社用端末でPython環境を整えるのが厳しい方や、とりあえず手軽にツールを使いたい人向けの実行ファイル(.exe)が格納されたzipファイルです。
以下のURLからダウンロードできます。
https://drive.google.com/file/d/1cM3tBYAAWf72_6wnz9WbVcSxeU_rzZ0I/view?usp=sharing