📝

Pythonで実装するKindleのPDF化ー①スクリーンショット編

2024/12/30に公開

はじめに

Kindleの電子書籍をPDF化したいと思ったことはありませんか。
私は教科書など、文字を書き込みながら読みたい本は今回紹介する方法を使ってPDF化し、タブレットに読み込んで閲覧しています。

KindleのPDF化は大きく分けて以下の3つの作業に分かれます。

  1. すべてのページのスクリーンショットを撮る。
  2. 適切に余白を削除して整える。
  3. 整えた画像ファイルを一つのPDFに変換する。

以下のページでそれぞれのやり方を説明しているので、ご興味のある方は参考にしてみてください。(当たり前ですが、PDF化したものの著作権にはお気をつけください。)

Pythonで実装するKindleのPDF化ー①スクリーンショット編
Pythonで実装するKindleのPDF化ー②トリミング編
Pythonで実装するKindleのPDF化ー③PDF変換編

なお、今回紹介するコードは以下の記事でご紹介いただいたものに、自分なりにアレンジを加えたものになります。
Kindle for PCのスクショを撮る #Python - Qiita
本家様の記事のコードの方が完成度は高いので、ぜひそちらもご覧ください。
作成者様ありがとうございました。

スクリーンショット編

KindleのPDF化について、今回はスクリーンショット編です。

この記事では、PythonとSeleniumを使って、Kindle PCアプリケーションの画面を自動的にキャプチャし保存するプログラムの作り方を解説します。
Claudeを使いながらコードの解説を書いているので、少しわかりにくい日本語になっていたらすみません。
少々複雑になりますが、よくわからない方はこのコードをそのまま張り付けてもある程度動くかと思います。

プログラムの概要

できること

このプログラムには以下の機能があります。

  1. Kindle PCアプリのウィンドウを自動検出
  2. ウィンドウの適切な位置を特定してキャプチャ
  3. 自動でページめくりを行いながら連続撮影
  4. 撮影した画像を指定したフォルダに保存

必要な環境とライブラリ

まず、以下のライブラリをインストールしておきましょう。

pip install pyautogui
pip install Pillow
pip install opencv-python

プログラムの実装

1. 必要なライブラリのインポート

import pyautogui as pag
import os, os.path as osp
import datetime, time
from PIL import ImageGrab
from tkinter import messagebox, simpledialog, filedialog
import cv2
import numpy as np
from ctypes import *
from ctypes.wintypes import *

各ライブラリの役割は以下の通りです。

  • pyautogui: マウスとキーボードの自動操作を行うためのライブラリです。
    • このプログラムでは、Kindleウィンドウをクリックしてフォーカスを設定したり、ページめくり操作を自動化するために使用しています。
  • os: ファイル・ディレクトリの操作を行うためのライブラリです。
    • キャプチャした画像を保存するフォルダを作成したり、フォルダ内のファイルを操作するために使用しています。
  • PIL.ImageGrab: スクリーンショットを取得するためのライブラリです。
    • 画面全体のスクリーンショットをキャプチャするために使用しています。
  • tkinter: ダイアログボックスを表示するためのライブラリです。
    • ユーザーにタイトルや保存先フォルダを入力してもらうためのダイアログボックスを表示するために使用しています。
  • cv2: 画像処理を行うためのライブラリです。
    • キャプチャした画像の切り取りや、色形式の変換などの画像処理を行うために使用しています。
  • numpy: 画像データの配列処理を行うためのライブラリです。
    • キャプチャした画像データを効率的に処理するために使用しています。
    • 画像を配列として扱うことで、高速な処理が可能になります。
  • ctypes: WindowsのAPIを利用したウィンドウ操作を行うためのライブラリです。
    • 画面上のウィンドウを検索したり、ウィンドウを前面に表示したりするなどの操作を行うために使用しています。

2. グローバル設定

# グローバル変数
kindle_window_title = 'Kindle for PC'  # Kindle for PCのウィンドウタイトル
page_change_key = 'right'    # 次のページに移動するキー
kindle_fullscreen_wait = 5   # フルスクリーンにした後の待ち時間(秒)
l_margin = 1                 # サイズ自動設定のときの左側マージン
r_margin = 1                 # サイズ自動設定のときの右側マージン
waitsec = 0.15              # キーを押してからスクリーンショットを撮る待ち時間(秒)

これらの設定値は、皆様の環境に応じて調整が必要になる可能性があります。特に以下の点に注意が必要です。

  • kindle_window_titleKindle for PCを指定していれば大丈夫だと思いますが、うまく動かない場合はウィンドウタイトルを修正してください。
  • page_change_key:縦書きの本など左めくりの場合はleftを指定します。
  • kindle_fullscreen_wait: PCの性能によって待機時間を調整
  • waitsec:ページめくりや読み込みの速度に応じて調整

3. Kindleウィンドウの検出機能

まず、画面上に表示されているKindleウィンドウを見つけ出す機能を実装します。
この関数は、Windows APIを使用して画面上のウィンドウを列挙し、タイトルに Kindle for PCが含まれるウィンドウを探します。
見つかった場合、そのウィンドウのハンドル(識別子)を返します。
見つからない場合は None を返します。

def find_kindle_window():
    """
    Kindleウィンドウを見つけて、そのハンドルを返す関数
    """
    # Windows APIの関数を取得
    EnumWindows = windll.user32.EnumWindows
    GetWindowText = windll.user32.GetWindowTextW
    GetWindowTextLength = windll.user32.GetWindowTextLengthW
    WNDENUMPROC = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int))
    
    # ウインドウ識別子の初期化
    ghwnd = None
    
    # ウィンドウを列挙する関数を定義
    def EnumWindowsProc(hwnd, lParam):
        nonlocal ghwnd
        length = GetWindowTextLength(hwnd)
        buff = create_unicode_buffer(length + 1)
        GetWindowText(hwnd, buff, length + 1)
        if kindle_window_title in buff.value:
            ghwnd = hwnd
            return False
        return True
    
    # ウィンドウの列挙を実行して結果を返す
    EnumWindows(WNDENUMPROC(EnumWindowsProc), 0)
    return ghwnd

この関数では、以下のような処理を行っています。

  1. Windows APIの関数を取得(5-9行目):

    • EnumWindows: システム上の全てのトップレベルウィンドウを列挙するための関数です。
    • GetWindowTextW: ウィンドウのタイトルテキストを取得するUnicode対応の関数です。
    • GetWindowTextLength: ウィンドウタイトルの長さを取得する関数です。
    • WINFUNCTYPE: コールバック関数の型を定義するために使用します。
  2. ウィンドウ識別子の初期化(11-12行目):

    • ghwnd変数をNoneで初期化し、Kindleウィンドウが見つかった際にここにハンドルを格納します。
    • この変数はコールバック関数内からアクセスできるよう、関数スコープで定義されています。
  3. ウィンドウ列挙用コールバック関数の定義(14-22行目):

    • EnumWindowsProc関数は各ウィンドウに対して呼び出されます。
    • nonlocal ghwndで外部スコープの変数を参照可能にします。
    • create_unicode_bufferでウィンドウタイトルを格納するバッファを作成します。
    • タイトルにkindle_window_titleが含まれる場合、そのハンドルを保存してFalseを返し列挙を終了します。
    • それ以外の場合はTrueを返して次のウィンドウの列挙を継続します。
  4. ウィンドウ列挙の実行(24-25行目):

    • EnumWindows関数にコールバック関数を渡して実行します。
    • コールバック関数はWNDENUMPROCでラップされ、正しい形式のC関数ポインタとして渡されます。
    • 見つかったKindleウィンドウのハンドル、またはNoneを返します。

この部分は少し難しいと思いますが、きちんと理解できていなくても、とりあえずは動きますので大丈夫です。(私もChatGPTに聞かないと理解できていません)

4. ウィンドウの設定機能

見つけたKindleウィンドウを操作可能な状態にする機能を実装します。
スクリーンショットを連続して撮影するためには、Kindleウィンドウが最前面にあり、かつ操作可能な状態になっている必要があります。
この機能では、Windowsのシステム機能とマウス操作を組み合わせて、ウィンドウを適切な状態に設定します。

def setup_kindle_window(hwnd):
    """
    Kindleウィンドウを前面に出し、フォーカスを設定する関数
    
    引数:
        hwnd: Kindleウィンドウのハンドル(識別子)
    """
    # Windows APIの関数を取得
    SetForegroundWindow = windll.user32.SetForegroundWindow
    GetWindowRect = windll.user32.GetWindowRect
    
    # ウィンドウを最前面に表示
    SetForegroundWindow(hwnd)
    
    # ウィンドウの位置と大きさを取得
    rect = RECT()
    GetWindowRect(hwnd, pointer(rect))
    
    # クリック位置を設定してフォーカスを与える
    pag.moveTo(rect.left+60, rect.top + 10)
    pag.click()
    
    # 設定が反映されるまで待機
    time.sleep(1)

この関数では、以下の重要な処理を順番に実行しています。

  1. Windows APIの準備(6-8行目):

    • SetForegroundWindow: ウィンドウを最前面に表示するための関数を取得します。
    • GetWindowRect: ウィンドウの位置と大きさを取得するための関数を取得します。
    • これらの関数はwindll.user32ライブラリから取得し、Windowsシステムの機能を直接利用します。
  2. ウィンドウを最前面に表示(11行目):

    • SetForegroundWindow(hwnd)を実行して、Kindleウィンドウを画面の最前面に持ってきます。
    • これは他のウィンドウの上にKindleウィンドウを表示する処理です。
    • hwndは前の関数で取得したKindleウィンドウの識別子です。
  3. ウィンドウの位置情報取得(14-15行目):

    • RECT()でウィンドウの座標を格納するための構造体を作成します。
    • GetWindowRectでウィンドウの位置情報(左上と右下の座標)を取得します。
    • この情報は次のマウス操作で使用されます。
  4. フォーカスの設定(18-19行目):

    • pag.moveTo: マウスカーソルをウィンドウの左上から少し離れた位置(左に60ピクセル、上に10ピクセル)に移動します。
    • pag.click(): その位置でマウスクリックを実行します。
    • この操作によってウィンドウが確実にアクティブになり、キーボード入力を受け付ける状態になります。
  5. 待機処理(22行目):

    • time.sleep(1)で1秒間待機します。
    • これは各設定が確実にWindowsシステムに反映されるのを待つための処理です。
    • この待機がないと、後続の操作が正しく動作しない可能性があります。

この関数は、後続のスクリーンショット撮影やページめくり操作が確実に実行できるよう、Kindleウィンドウを適切な状態に準備する重要な役割を果たしています。
Windows APIとPyAutoGUI(pag)を組み合わせることで、より確実なウィンドウ操作を実現しています。

5. コンテンツ境界の検出機能

キャプチャする領域を正確に特定するための機能です。
この機能は、ページ内の実際のコンテンツ領域(本文部分)と余白を区別し、必要な部分のみを抽出するために使用されます。
なお、改めて別のコードでトリミングを行うため、うまく領域が特定できていなくても大丈夫です。

def find_content_boundaries(img):
    """
    画像内のコンテンツの境界を見つける関数
    
    Parameters:
        img (numpy.ndarray): 解析する画像データ(OpenCV形式のBGR画像)
    
    Returns:
        tuple: コンテンツの左端(lft)と右端(rht)の座標
    """
    # ピクセルを比較して境界を見つける関数
    def cmps(img, rng):
        for i in rng:
            # 背景色と異なる色が見つかった位置を返す
            # img[20][i]は走査している位置のピクセル
            # img[19][0]は基準となる背景色のピクセル
            if np.all(img[20][i] != img[19][0]):
                return i
        
    # 左端と右端の境界を検出
    lft = cmps(img, range(l_margin, img.shape[1]-r_margin))
    rht = cmps(img, reversed(range(l_margin, img.shape[1]-r_margin)))
    return lft, rht

この関数の処理を詳しく解説していきます。

  1. ピクセル比較関数cmpsの定義(7-14行目):

    • この内部関数は、画像の特定の行(y=20の位置)のピクセルを順番に確認します。
    • 比較の基準として、y=19、x=0の位置のピクセル(通常は背景色)を使用します。
    • np.all()を使用して、RGBの全ての色成分が基準と異なるピクセルを探します。
    • 異なる色が見つかった場合、その水平位置(x座標)を返します。
    • この方法により、背景色から本文色への変化点を検出できます。
  2. 境界の検出処理(17-19行目):

    • 左端の検出:
      • range(l_margin, img.shape[1]-r_margin)で、左マージンから右マージンまでの範囲を生成します。
      • 左から右へ順次走査し、最初にコンテンツが見つかった位置を左端として記録します。
    • 右端の検出:
      • reversed(range(...))で、同じ範囲を右から左へ走査します。
      • 右から左へ走査し、最初にコンテンツが見つかった位置を右端として記録します。
    • 検出された左端(lft)と右端(rht)の座標をタプルとして返します。

6. 画面キャプチャと保存機能

プログラムの核となる、実際のキャプチャ処理と保存を行う機能を実装します。
この機能は、電子書籍や文書の各ページを自動的にキャプチャし、連番付きの画像ファイルとして保存します。

def capture_and_save_pages(lft, rht, title):
    """
    ページをキャプチャして保存する関数
    
    Parameters:
        lft (int): キャプチャする領域の左端のx座標
        rht (int): キャプチャする領域の右端のx座標
        title (str): 保存するフォルダ名
    
    Returns:
        int: 保存したページ数
    """
    # 画面サイズを取得して前回の画像用の配列を初期化
    sc_h, * = get_screen_size()
    old = np.zeros((sc_h, rht-lft, 3), np.uint8)
    page = 1
    # 保存先フォルダの設定
    cd = os.getcwd()  # 現在のディレクトリを保存
    os.mkdir(osp.join(base_save_folder, title))  # 新しいフォルダを作成
    os.chdir(osp.join(base_save_folder, title))  # 作成したフォルダに移動
    
    while True:
        # ファイル名を設定(001.png, 002.png...)
        filename = f"{page:03d}.png"
        start = time.perf_counter()  # 処理開始時刻を記録
        
        while True:
            # ページめくりのアニメーション待機
            time.sleep(waitsec)
            
            # スクリーンショットを撮影して処理
            s = ImageGrab.grab()  # 画面全体をキャプチャ
            s = np.array(s)  # NumPy配列に変換
            ss = cv2.cvtColor(s, cv2.COLOR_RGB2BGR)  # 色形式をBGRに変換
            ss = ss[:, lft: rht]  # 余白を切り取り
            
            # 画像が変化したかチェック
            if not np.array_equal(old, ss):
                break
            
            # タイムアウト処理(5秒以上変化がない場合は終了)
            if time.perf_counter() - start > 5.0:
                os.chdir(cd)
                return page - 1
        
        # 画像を保存して次のページへ
        cv2.imwrite(filename, ss)
        old = ss
        print(f'Page: {page}, {ss.shape}, {time.perf_counter() - start:.2f} sec')
        page += 1
        pag.keyDown(page_change_key)

この関数の処理を詳しく解説していきます。

  1. 初期設定部分(7-9行目):

    • get_screen_size()関数で現在の画面の高さを取得します。
    • np.zeros()で前回の画像との比較用の空の配列を作成します。配列のサイズは画面の高さと指定された幅(rht-lft)に合わせます。
    • ページのカウンターを1に初期化します。
  2. 保存準備(11-14行目):

    • os.getcwd()で現在の作業ディレクトリのパスを保存します。
    • os.mkdir()でタイトル名を使用して新しいフォルダを作成します。
    • os.chdir()で作成したフォルダに移動し、以降の保存処理の基準とします。
  3. キャプチャループ(16-44行目):

    • f文字列を使用して、ページ番号を3桁の連番ファイル名に変換します(例:001.png)。
    • time.perf_counter()で処理開始時刻を記録し、タイムアウト判定に使用します。
    • 内部ループでは:
      • time.sleep()でページめくりアニメーションの完了を待機します。
      • ImageGrab.grab()で画面全体をキャプチャします。
      • NumPy配列に変換後、OpenCVで扱えるBGR形式に変換します。
      • スライシング操作で指定された領域(lft:rht)のみを切り出します。
      • np.array_equal()で前回の画像と比較し、変化があれば次の処理へ進みます。
      • 5秒以上変化がない場合は、全ページのキャプチャが完了したとみなして処理を終了します。
  4. 保存処理(46-51行目):

    • cv2.imwrite()で処理した画像をPNG形式で保存します。
    • 現在の画像をold変数に保存し、次回の比較対象とします。
    • コンソールに進捗状況(ページ番号、画像サイズ、処理時間)を表示します。
    • pyautoguiを使用して次のページに進むキーを押下します。

この実装により自動的にページめくりしながら連続的にキャプチャすることが可能になります。
また、タイムアウト機能により、最終ページでの自動停止も実現しています。

7. メイン関数の実装

プログラム全体の流れを制御するメイン関数を実装します。
この関数は、Kindleアプリのウィンドウ制御から画像キャプチャまでの一連の処理を統括し、エラーハンドリングも行います。

def main():
    """
    メイン関数:全体の処理を制御する
    
    Returns:
        None: 処理が正常に完了した場合
        
    Raises:
        - Kindleウィンドウが見つからない場合はエラーメッセージを表示して終了
        - 保存先フォルダが未選択の場合はエラーメッセージを表示して終了
    """
    global base_save_folder
    # Kindleウィンドウの検索と設定
    hwnd = find_kindle_window()
    if hwnd is None:
        messagebox.showerror("エラー", "Kindleが見つかりません")
        return
    setup_kindle_window(hwnd)
    
    # 画面設定とマウス位置の調整
    sc_w, sc_h = get_screen_size()
    pag.moveTo(sc_w - 200, sc_h - 1)  # マウスカーソルを画面右下に移動
    time.sleep(kindle_fullscreen_wait)  # フルスクリーン遷移の完了を待機
    
    # 保存設定の取得
    title = get_title()  # ユーザーからタイトルを取得
    base_save_folder = get_save_folder()  # 保存先フォルダのパスを取得
    if not base_save_folder:
        messagebox.showerror("エラー", "保存先フォルダが選択されていません")
        return
    
    # 初期画像の取得と境界検出
    img = ImageGrab.grab()  # 画面全体をキャプチャ
    img = np.array(img)  # NumPy配列に変換
    imp = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)  # OpenCV形式に変換
    lft, rht = find_content_boundaries(imp)  # コンテンツ領域の境界を検出
    
    # キャプチャ実行と完了通知
    total_pages = capture_and_save_pages(lft, rht, title)
    messagebox.showinfo(
        "完了", 
        f"スクリーンショットの撮影が終了しました。\n合計 {total_pages} ページを保存しました。"
    )

メイン関数の処理を詳しく解説していきます。

  1. Kindleウィンドウの検索と初期設定(8-14行目):

    • find_kindle_window()でKindleアプリのウィンドウハンドルを取得します。
    • ウィンドウが見つからない場合は、エラーメッセージを表示して処理を中断します。
    • setup_kindle_window()でウィンドウを操作可能な状態に設定します。
  2. 画面設定の調整(16-18行目):

    • get_screen_size()で現在の画面サイズを取得します。
    • pyautogui.moveTo()でマウスカーソルを画面右下に移動し、UIの自動非表示を促します。
    • kindle_fullscreen_wait秒間待機し、フルスクリーン表示の完了を待ちます。
  3. 保存設定の取得(20-25行目):

    • get_title()でユーザーからキャプチャする書籍のタイトルを取得します。
    • get_save_folder()で画像の保存先フォルダを取得します。
    • 保存先が未選択の場合は、エラーメッセージを表示して処理を中断します。
  4. 初期画像の取得と境界検出(27-30行目):

    • ImageGrab.grab()で現在の画面をキャプチャします。
    • NumPy配列に変換後、OpenCVで処理可能なBGR形式に変換します。
    • find_content_boundaries()でコンテンツ領域の左端と右端の座標を特定します。
  5. キャプチャの実行と完了通知(32-37行目):

    • capture_and_save_pages()で検出された範囲の連続キャプチャを実行します。
    • キャプチャが完了したら、保存したページ数を含む完了メッセージを表示します。

実行方法

まず、このページからKindle for PCをインストールしてください。
Amazon.co.jp: Kindle無料アプリ: Kindleストア

インストールが完了したら、スクリーンショットを撮りたい本を開き、全画面表示にした後でこのコードを実行するだけです。
今回は青空文庫から江戸川乱歩の「一人二役」をPDF化してみようと思います。
Amazon.co.jp: 一人二役 電子書籍: 江戸川 乱歩: Kindleストア

注意点として、スマホでも閲覧する可能性がある場合は、見開き表示ではなく1ページずつの表示にしましょう。
見開き表示でPDFを作ってしまうと、スマホなどの縦にスクロールするデバイスではとても見にくくなります。
表示方法の変更は上部メニューの「移動」の横にある窓のようなマークを押せば変更できます。

コードを実行するとこのようなGUIが表示されます。

作成するフォルダの名前になるので適当に入力してください。
私は面倒くさいのでいつも空欄にします。
その後、保存先フォルダについて聞かれるので適切な場所を選択したら、あとはスクリーンショットが勝手に撮られていきます。
この間はパソコンには触れないようにしましょう。

すべての撮影が完了すると以下のようなポップアップが出ますので、「OK」を押して終了です。

ダウンロード先を見てみると、このように日付形式のフォルダが作成されています。

中を見ると、このように適切に1ページずつスクリーンショットが撮影できています。

まとめ

このプログラムを使用することで、Kindle PCアプリの画面を自動的にキャプチャし保存することができます。
まだこの状態だと、画像に不要な余白が含まれてしまっているので、次に、この画像をトリミングするコードを実装していきます。

Pythonで実装するKindleのPDF化ー①スクリーンショット編
Pythonで実装するKindleのPDF化ー②トリミング編
Pythonで実装するKindleのPDF化ー③PDF変換編

コード全文

コード全文はこちらにまとめておきます。

"""
Kindle PCアプリケーションの画面を自動的にキャプチャし、保存するプログラム

主な機能:
1. Kindleウィンドウの自動検出
2. 画面のキャプチャと保存
3. 自動ページめくり
4. 進捗状況の表示

使用方法:
1. Kindleアプリを起動し、目的の本を開く
2. フルスクリーンモードにする
3. このプログラムを実行
4. タイトルと保存先フォルダを指定
5. 自動キャプチャが開始

注意:
- 個人使用目的のプログラムです
- 著作権に配慮して使用してください
"""

# 必要なライブラリのインポート
import pyautogui as pag
import os, os.path as osp
import datetime, time
from PIL import ImageGrab
from tkinter import messagebox, simpledialog, filedialog
import cv2
import numpy as np
from ctypes import *
from ctypes.wintypes import *

# グローバル変数の設定
kindle_window_title = 'Kindle for PC'  # Kindle for PCのウィンドウタイトル
page_change_key = 'right'      # 次のページへ移動するキー
kindle_fullscreen_wait = 5     # フルスクリーン後の待機時間(秒)
l_margin = 1                   # 左側マージン
r_margin = 1                   # 右側マージン
waitsec = 0.15                 # キー押下後の待機時間(秒)

def find_kindle_window():
    """
    Kindleウィンドウを検索してハンドルを返す関数
    
    Returns:
        ghwnd: Kindleウィンドウのハンドル。見つからない場合はNone
    """
    # Windows APIの関数を取得
    EnumWindows = windll.user32.EnumWindows
    GetWindowText = windll.user32.GetWindowTextW
    GetWindowTextLength = windll.user32.GetWindowTextLengthW
    WNDENUMPROC = WINFUNCTYPE(c_bool, POINTER(c_int), POINTER(c_int))
    
    ghwnd = None
    
    def EnumWindowsProc(hwnd, lParam):
        """ウィンドウ列挙のためのコールバック関数"""
        nonlocal ghwnd
        length = GetWindowTextLength(hwnd)
        buff = create_unicode_buffer(length + 1)
        GetWindowText(hwnd, buff, length + 1)
        if kindle_window_title in buff.value:
            ghwnd = hwnd
            return False
        return True
    
    EnumWindows(WNDENUMPROC(EnumWindowsProc), 0)
    return ghwnd

def setup_kindle_window(hwnd):
    """
    Kindleウィンドウを前面に表示しフォーカスを設定
    
    Args:
        hwnd: ウィンドウハンドル
    """
    SetForegroundWindow = windll.user32.SetForegroundWindow
    GetWindowRect = windll.user32.GetWindowRect
    
    # ウィンドウを前面に表示
    SetForegroundWindow(hwnd)
    rect = RECT()
    GetWindowRect(hwnd, pointer(rect))
    
    # クリックしてフォーカスを設定
    pag.moveTo(rect.left+60, rect.top + 10)
    pag.click()
    time.sleep(1)

def get_screen_size():
    """画面サイズを取得"""
    return pag.size()

def get_title():
    """
    保存用のタイトルを取得
    空の場合は現在時刻を使用
    """
    default_title = str(datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
    tt = simpledialog.askstring('タイトルを入力','タイトルを入力して下さい(空白の場合現在の時刻)')
    return tt if tt != '' else default_title

def get_save_folder():
    """保存先フォルダを選択"""
    return filedialog.askdirectory(title='保存するフォルダを選択してください')

def find_content_boundaries(img):
    """
    画像内のコンテンツ境界を検出
    
    Args:
        img: 画像データ(NumPy配列)
    Returns:
        lft: 左端の位置
        rht: 右端の位置
    """
    def cmps(img, rng):
        """ピクセルの色を比較して境界を検出"""
        for i in rng:
            if np.all(img[20][i] != img[19][0]):
                return i
    
    lft = cmps(img, range(l_margin, img.shape[1]-r_margin))
    rht = cmps(img, reversed(range(l_margin, img.shape[1]-r_margin)))
    return lft, rht

def capture_and_save_pages(lft, rht, title):
    """
    ページをキャプチャして保存
    
    Args:
        lft: 左端の位置
        rht: 右端の位置
        title: 保存時のタイトル
    Returns:
        page - 1: 保存したページ数
    """
    # 画面サイズ取得と初期化
    sc_h, _ = get_screen_size()
    old = np.zeros((sc_h, rht-lft, 3), np.uint8)
    page = 1

    # 保存先フォルダの設定
    cd = os.getcwd()
    os.mkdir(osp.join(base_save_folder, title))
    os.chdir(osp.join(base_save_folder, title))
    
    while True:
        # ファイル名設定と時間計測開始
        filename = f"{page:03d}.png"
        start = time.perf_counter()
        
        while True:
            # ページめくり後の待機
            time.sleep(waitsec)
            
            # スクリーンショット取得と処理
            s = ImageGrab.grab()
            s = np.array(s)
            ss = cv2.cvtColor(s, cv2.COLOR_RGB2BGR)
            ss = ss[:, lft: rht]
            
            # ページめくり完了を確認
            if not np.array_equal(old, ss):
                break
            
            # タイムアウト処理
            if time.perf_counter() - start > 5.0:
                os.chdir(cd)
                return page - 1
        
        # 画像保存と次ページへ
        cv2.imwrite(filename, ss)
        old = ss
        print(f'Page: {page}, {ss.shape}, {time.perf_counter() - start:.2f} sec')
        page += 1
        pag.keyDown(page_change_key)

def main():
    """メイン処理"""
    global base_save_folder

    # Kindleウィンドウを探索
    hwnd = find_kindle_window()
    if hwnd is None:
        messagebox.showerror("エラー", "Kindleが見つかりません")
        return

    # ウィンドウの設定
    setup_kindle_window(hwnd)

    # 画面サイズを取得してマウス移動
    sc_w, sc_h = get_screen_size()
    pag.moveTo(sc_w - 200, sc_h - 1)
    time.sleep(kindle_fullscreen_wait)

    # タイトルと保存先の取得
    title = get_title()
    base_save_folder = get_save_folder()
    if not base_save_folder:
        messagebox.showerror("エラー", "保存先フォルダが選択されていません")
        return

    # 初期画像を取得して境界を検出
    img = ImageGrab.grab()
    img = np.array(img)
    imp = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
    lft, rht = find_content_boundaries(imp)

    # キャプチャを実行
    total_pages = capture_and_save_pages(lft, rht, title)
    
    # 完了メッセージを表示
    messagebox.showinfo("完了", 
                       f"スクリーンショットの撮影が終了しました。\n"
                       f"合計 {total_pages} ページを保存しました。")

if __name__ == "__main__":
    main()

Discussion