PDF Pythonツール アニメーションgif

PythonでPDFからアニメーションGIFを作成する便利ツールを作成

概要

PDFファイルからアニメーションGIFを簡単に作成できる「PDF to GIF Converter」をご紹介します。このPythonプログラムを使用すると、PDFの特定のページ範囲を選択し、アニメーションGIFとして保存することができます。また、選択した範囲を新しいPDFとして保存することも可能です。

本記事では、このツールの使用例、インストール方法、使用手順、注意点、そしてプログラムコードを詳しく解説します。

使用例

PDF to GIF Converterは以下のような場面で活用できます:

  1. プレゼンテーション資料の一部をアニメーションGIFとして抽出
  2. 複数ページにわたる図表をアニメーションGIFに変換
  3. PDFマニュアルの特定セクションをGIFアニメーションとして作成
  4. PDFの特定ページや範囲を新しいPDFファイルとして保存

インストール方法

PDF to GIF Converterを使用するには、以下の手順でPythonと必要なライブラリをインストールします:

  1. Python公式サイトからPython 3.7以降をダウンロードしてインストール
  2. コマンドプロンプトまたはターミナルを開き、以下のコマンドを実行して必要なライブラリをインストール:
pip install tkinterdnd2 Pillow PyMuPDF

使用手順

  1. プログラムを実行し、GUIウィンドウを起動
  2. PDFファイルをドラッグ&ドロップエリアにドロップ
  3. 表示されたPDFページ上で範囲を選択(選択しない場合はページ全体が対象)
  4. フレーム間隔を設定(GIFアニメーションの場合)
  5. ページ順序を選択(「前から後ろ」または「後ろから前」)
  6. 「GIFとして保存」または「PDFとして保存」ボタンをクリック
  7. 保存先を選択し、ファイルを保存

注意点

  • 大きなPDFファイルの処理には時間がかかる場合があります
  • 生成されるGIFファイルのサイズが大きくなる可能性があるため、選択範囲は必要最小限にすることをおすすめします
  • PDFファイルによっては、セキュリティ設定により変換できない場合があります
  • 著作権に配慮し、許可された資料のみ変換するようにしてください

プログラム

import tkinter as tk
from tkinter import filedialog, messagebox
from tkinterdnd2 import DND_FILES, TkinterDnD
from PIL import Image, ImageTk
import fitz  # PyMuPDF
import io
import os
import tempfile
import traceback
import sys

class PDFtoGIFConverter:
    def __init__(self, master):
        self.master = master
        self.master.title("PDF Converter")
        self.master.geometry("800x700")

        self.pdf_path = None
        self.output_path = None
        self.pdf_document = None
        self.current_page = 0
        self.selection_start = None
        self.selection_rect = None
        self.page_order = tk.StringVar(value="forward")
        self.page_dimensions = (0, 0)

        self.setup_ui()

    def setup_ui(self):
        main_frame = tk.Frame(self.master)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)

        left_frame = tk.Frame(main_frame, width=200)
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))

        self.drop_area = tk.Label(left_frame, text="PDFファイルをここにドラッグ", bg="lightgray", height=3, width=25)
        self.drop_area.pack(fill=tk.X, pady=(0, 10))
        self.drop_area.drop_target_register(DND_FILES)
        self.drop_area.dnd_bind('<<Drop>>', self.on_drop)

        self.pdf_label = tk.Label(left_frame, text="選択されたPDF: なし", wraplength=180)
        self.pdf_label.pack(fill=tk.X, pady=(0, 10))

        tk.Label(left_frame, text="フレーム間隔(秒):").pack()
        self.interval_entry = tk.Entry(left_frame)
        self.interval_entry.pack(pady=(0, 10))
        self.interval_entry.insert(0, "1.0")

        tk.Label(left_frame, text="ページ順序:").pack()
        tk.Radiobutton(left_frame, text="前から後ろ", variable=self.page_order, value="forward").pack()
        tk.Radiobutton(left_frame, text="後ろから前", variable=self.page_order, value="backward").pack()

        self.selection_label = tk.Label(left_frame, text="範囲指定: ページ全体")
        self.selection_label.pack(pady=(10, 0))

        self.reset_button = tk.Button(left_frame, text="範囲指定をリセット", command=self.reset_selection, state=tk.DISABLED)
        self.reset_button.pack(pady=(10, 0))

        self.save_gif_button = tk.Button(left_frame, text="GIFとして保存", command=self.save_gif, state=tk.DISABLED)
        self.save_gif_button.pack(pady=(10, 0))

        self.save_pdf_button = tk.Button(left_frame, text="PDFとして保存", command=self.save_pdf, state=tk.DISABLED)
        self.save_pdf_button.pack(pady=(10, 0))

        self.viewer_frame = tk.Frame(main_frame, bg="white")
        self.viewer_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        self.canvas = tk.Canvas(self.viewer_frame, bg="white")
        self.canvas.pack(fill=tk.BOTH, expand=True)
        self.canvas.bind("<ButtonPress-1>", self.on_press)
        self.canvas.bind("<B1-Motion>", self.on_drag)
        self.canvas.bind("<ButtonRelease-1>", self.on_release)

        self.debug_label = tk.Label(self.master, text="", wraplength=780, justify="left")
        self.debug_label.pack(pady=5)

    def show_error(self, message, exception):
        full_message = f"{message}\n\nエラー詳細:\n{str(exception)}\n\n{traceback.format_exc()}"
        print(full_message, file=sys.stderr)
        self.debug_label.config(text=full_message)
        messagebox.showerror("エラー", message)

    def on_drop(self, event):
        try:
            file_path = event.data.strip('{}').strip()
            if os.path.isfile(file_path) and file_path.lower().endswith('.pdf'):
                self.pdf_path = file_path
                self.pdf_label.config(text=f"選択されたPDF: {os.path.basename(self.pdf_path)}")
                self.load_pdf()
            else:
                raise ValueError("無効なファイル形式です")
        except Exception as e:
            self.show_error("ファイルのドロップ中にエラーが発生しました", e)

    def load_pdf(self):
        try:
            self.pdf_document = fitz.open(self.pdf_path)
            self.current_page = 0
            self.display_page()
            self.update_button_states()
        except Exception as e:
            self.show_error("PDFファイルの読み込み中にエラーが発生しました", e)

    def display_page(self):
        try:
            if self.pdf_document:
                page = self.pdf_document.load_page(self.current_page)
                pix = page.get_pixmap()
                img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
                self.tk_img = ImageTk.PhotoImage(image=img)
                self.canvas.config(width=pix.width, height=pix.height)
                self.canvas.create_image(0, 0, anchor=tk.NW, image=self.tk_img)
                self.page_dimensions = (pix.width, pix.height)
                self.selection_rect = None
                self.update_selection_label()
        except Exception as e:
            self.show_error("ページの表示中にエラーが発生しました", e)

    def on_press(self, event):
        self.selection_start = (event.x, event.y)
        if self.selection_rect:
            self.canvas.delete(self.selection_rect)
        self.selection_rect = None
        self.update_selection_label()

    def on_drag(self, event):
        if self.selection_start:
            x0, y0 = self.selection_start
            x1, y1 = event.x, event.y
            if self.selection_rect:
                self.canvas.coords(self.selection_rect, x0, y0, x1, y1)
            else:
                self.selection_rect = self.canvas.create_rectangle(x0, y0, x1, y1, outline="red")
            self.update_selection_label()

    def on_release(self, event):
        self.update_selection_label()
        self.update_button_states()

    def update_selection_label(self):
        if self.selection_rect:
            coords = self.canvas.coords(self.selection_rect)
            self.selection_label.config(text=f"範囲指定: {coords}")
        else:
            self.selection_label.config(text="範囲指定: ページ全体")

    def update_button_states(self):
        if self.pdf_document:
            self.reset_button.config(state=tk.NORMAL)
            self.save_gif_button.config(state=tk.NORMAL)
            self.save_pdf_button.config(state=tk.NORMAL)
        else:
            self.reset_button.config(state=tk.DISABLED)
            self.save_gif_button.config(state=tk.DISABLED)
            self.save_pdf_button.config(state=tk.DISABLED)

    def reset_selection(self):
        if self.selection_rect:
            self.canvas.delete(self.selection_rect)
        self.selection_rect = None
        self.selection_start = None
        self.update_selection_label()
        messagebox.showinfo("リセット完了", "範囲指定をリセットしました。ページ全体が選択されています。")

    def get_selection_coords(self):
        if self.selection_rect:
            return self.canvas.coords(self.selection_rect)
        else:
            return (0, 0, self.page_dimensions[0], self.page_dimensions[1])

    def save_gif(self):
        try:
            if not self.pdf_document:
                raise ValueError("PDFファイルを選択してください。")

            interval = round(float(self.interval_entry.get()), 1)
            if interval < 0.1:
                raise ValueError("間隔は0.1秒以上にしてください。")

            output_path = filedialog.asksaveasfilename(defaultextension=".gif", filetypes=[("GIF files", "*.gif")])
            if not output_path:
                return

            self.convert_pdf_to_gif(output_path, interval)
        except Exception as e:
            self.show_error("GIF保存中にエラーが発生しました", e)

    def convert_pdf_to_gif(self, output_path, interval):
        try:
            x0, y0, x1, y1 = map(int, self.get_selection_coords())

            images = []
            page_range = range(self.pdf_document.page_count)
            if self.page_order.get() == "backward":
                page_range = reversed(page_range)

            for page_num in page_range:
                page = self.pdf_document.load_page(page_num)
                pix = page.get_pixmap()
                img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
                img_cropped = img.crop((x0, y0, x1, y1))
                images.append(img_cropped)

            images[0].save(
                output_path,
                save_all=True,
                append_images=images[1:],
                duration=int(interval * 1000),
                loop=0
            )

            messagebox.showinfo("完了", f"アニメーションGIFを保存しました: {output_path}")
        except Exception as e:
            raise Exception(f"GIF変換中にエラーが発生しました: {str(e)}")

    def save_pdf(self):
        try:
            if not self.pdf_document:
                raise ValueError("PDFファイルを選択してください。")

            output_path = filedialog.asksaveasfilename(defaultextension=".pdf", filetypes=[("PDF files", "*.pdf")])
            if not output_path:
                return

            self.convert_pdf_to_pdf(output_path)
        except Exception as e:
            self.show_error("PDF保存中にエラーが発生しました", e)

    def convert_pdf_to_pdf(self, output_path):
        try:
            x0, y0, x1, y1 = map(float, self.get_selection_coords())

            output_doc = fitz.open()
            page_range = range(self.pdf_document.page_count)
            if self.page_order.get() == "backward":
                page_range = reversed(list(page_range))

            for page_num in page_range:
                page = self.pdf_document.load_page(page_num)
                new_page = output_doc.new_page(width=x1-x0, height=y1-y0)
                new_page.show_pdf_page(new_page.rect, self.pdf_document, page_num, clip=(x0, y0, x1, y1))

            toc = self.pdf_document.get_toc()
            new_toc = []
            for item in toc:
                level, title, page = item[:3]
                if self.page_order.get() == "backward":
                    page = self.pdf_document.page_count - page + 1
                if 1 <= page <= output_doc.page_count:
                    new_toc.append([level, title, page])

            output_doc.set_toc(new_toc)
            output_doc.save(output_path)
            output_doc.close()

            messagebox.showinfo("完了", f"選択範囲をPDFとして保存しました: {output_path}")
        except Exception as e:
            raise Exception(f"PDF変換中にエラーが発生しました: {str(e)}")

if __name__ == "__main__":
    try:
        root = TkinterDnD.Tk()
        app = PDFtoGIFConverter(root)
        root.mainloop()
    except Exception as e:
        print(f"アプリケーションの起動中にエラーが発生しました: {str(e)}\n\n{traceback.format_exc()}", file=sys.stderr)

あるいは、下のテキストファイルをダウンロードし、「.txt」を「.py」に変えることでそのまま使えます。

まとめ

PDF to GIF Converterを使用することで、PDFファイルから簡単にアニメーションGIFを作成したり、PDFの一部を抽出したりすることができます。プレゼンテーション資料の作成やマニュアルの一部を共有する際に非常に便利なツールです。

このプログラムはPythonの知識がなくても簡単に使用できますが、Pythonプログラミングに興味がある方にとっては、GUIアプリケーション開発やPDF処理の良い学習材料にもなるでしょう。

PDFファイルの効果的な活用や、情報の視覚化に興味がある方は、ぜひこのツールを試してみてください。また、このブログでは他にもPDFファイルの操作テクニックやPythonを使った業務効率化についての記事も公開していますので、ぜひチェックしてみてください。

-PDF, Pythonツール, アニメーションgif