0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kindleスクショ自動化ツールが動かなかったので修正してPNG化し、最後にPDF化した話

0
Last updated at Posted at 2026-04-16

Kindleスクショ自動化ツールが動かなかったので修正してPNG化し、最後にPDF化した話

はじめに

Kindle の内容を個人用途で整理したくて、参考記事をベースにアプリを動かしてみました。
ただ、そのままでは自分の環境でうまく動かなかったため、コードを修正して Windows 上で動作するようにしました。

最終的には、生成した PNG ファイルを Web サービスでまとめて PDF 化しました。

本記事は個人利用の範囲での検証メモです。著作権や利用規約には注意してください。


参考にした記事

以下の記事を参考にしました。

ただし、自分の環境ではそのままでは期待通りに動かず、いくつか修正が必要でした。


やりたかったこと

やりたかった流れは以下です。

  1. Kindle for PC を開く
  2. ページを自動でめくる
  3. 各ページを PNG で保存する
  4. 最後に PNG をまとめて PDF 化する

実行環境

  • Windows
  • Python 3.x
  • Kindle for PC

インストールしたパッケージ

以下のパッケージをインストールしました。

pip install pyautogui Pillow opencv-python numpy

今回使ったコードでは主に以下を利用しています。

  • pyautogui : マウス操作、キー入力
  • Pillow : 画面キャプチャ
  • opencv-python : 画像保存、画像比較
  • numpy : 画像データ比較

tkinterctypes は標準ライブラリなので、通常は追加インストール不要です。


うまく動かなかった点

元のコードをそのまま試したところ、以下のような点で安定しませんでした。

  • Kindle のウィンドウをうまく取得できない
  • ダイアログ操作後に Kindle からフォーカスが外れる
  • ページ送り直後にキャプチャしてしまい、同じ画面を拾うことがある
  • 最終ページの判定が弱く、終了条件が不安定

そのため、ウィンドウ操作とページ送り判定まわりを中心に修正しました。


修正した内容

1. Kindle for PC のウィンドウを取得する

ウィンドウタイトルに Kindle for PC を含むものを探して、対象ウィンドウを取得するようにしました。

kindle_window_title = 'Kindle for PC'

def find_kindle_window():
    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

2. ウィンドウを前面に出してフォーカスを合わせる

SetForegroundWindow だけでは前面に出せないことがあったので、Alt キーイベントを入れてから前面化し、さらにクリックして確実にフォーカスを合わせるようにしました。

def setup_kindle_window(hwnd):
    SetForegroundWindow = windll.user32.SetForegroundWindow
    GetWindowRect = windll.user32.GetWindowRect

    windll.user32.keybd_event(0x12, 0, 0, 0)
    windll.user32.keybd_event(0x12, 0, 0x0002, 0)

    SetForegroundWindow(hwnd)

    rect = RECT()
    GetWindowRect(hwnd, pointer(rect))

    sc_w, sc_h = pag.size()
    x = max(1, min(rect.left + 60, sc_w - 2))
    y = max(1, min(rect.top + 10, sc_h - 2))
    pag.moveTo(x, y)
    pag.click()
    time.sleep(1)

3. 画面全体ではなく Kindle ウィンドウだけをキャプチャする

ウィンドウ矩形を取得して、その範囲だけを ImageGrab.grab() でキャプチャするようにしました。

GetWindowRect = windll.user32.GetWindowRect
rect = RECT()
GetWindowRect(hwnd, pointer(rect))
bbox = (rect.left, rect.top, rect.right, rect.bottom)

これで、他ウィンドウの影響を受けにくくなりました。

4. ページ送り完了を画像差分で判定する

キーを送った直後に保存するのではなく、前回画像と違う画面になったことを確認してから保存するようにしました。

while True:
    time.sleep(waitsec)
    s = ImageGrab.grab(bbox=bbox)
    ss = cv2.cvtColor(np.array(s), cv2.COLOR_RGB2BGR)

    if not np.array_equal(old, ss):
        break

    if time.perf_counter() - start > 5.0:
        os.chdir(cd)
        return page - 1

この待ちを入れないと、同じ画面を連続保存したり、ページ切り替え途中の画像を拾ったりしやすかったです。

5. 同じ画像が続いたら終了する

前回保存した画像と今回の画像が同じなら、最後のページに到達したとみなして終了するようにしました。

if prev_saved is not None and np.array_equal(prev_saved, ss):
    os.chdir(cd)
    return page - 1

補足: ページ送りキーは Kindle のデータによって変わる

今回のコードでは、次ページへ移動するキーを以下で指定しています。

page_change_key = 'left'  # 次のページへ移動するキー

ただし、ここは固定ではなく、Kindle のデータによって leftright のどちらが次ページ送りになるかが異なる場合がありました。

例えば、

  • left で次ページに進むケース
  • right で次ページに進むケース

の両方がありました。

そのため、うまくページ送りできない場合は、この値を切り替えて試す必要があります。

page_change_key = 'left'

または

page_change_key = 'right'

ページ送りが逆になっていると、同じページを取り続けたり、期待した順番で保存できなかったりするので、最初に数ページだけ試して確認するのがおすすめです。


実際に使ったコード

# 必要なライブラリのインポート
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 *

# グローバル変数の設定
pag.FAILSAFE = False               # フェイルセーフを無効化
kindle_window_title = 'Kindle for PC'  # Kindle for PCのウィンドウタイトル
page_change_key = 'left'       # 次のページへ移動するキー
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制限を回避するためAltキーイベントを送出
    windll.user32.keybd_event(0x12, 0, 0, 0)        # Alt キー押下
    windll.user32.keybd_event(0x12, 0, 0x0002, 0)   # Alt キー解放

    # ウィンドウを前面に表示
    SetForegroundWindow(hwnd)

    rect = RECT()
    GetWindowRect(hwnd, pointer(rect))

    # クリックしてフォーカスを設定
    sc_w, sc_h = pag.size()
    x = max(1, min(rect.left + 60, sc_w - 2))
    y = max(1, min(rect.top + 10, sc_h - 2))
    pag.moveTo(x, y)
    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 capture_and_save_pages(bbox, title):
    """
    ページをキャプチャして保存
    Args:
        bbox: キャプチャ範囲 (left, top, right, bottom)
        title: 保存時のタイトル
    Returns:
        page - 1: 保存したページ数
    """
    left, top, right, bottom = bbox
    h, w = bottom - top, right - left
    old = np.zeros((h, w, 3), np.uint8)
    page = 1
    prev_saved = None

    # 保存先フォルダの設定
    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)

            # Kindleウィンドウ領域のみキャプチャ
            s = ImageGrab.grab(bbox=bbox)
            ss = cv2.cvtColor(np.array(s), cv2.COLOR_RGB2BGR)

            # ページめくり完了を確認
            if not np.array_equal(old, ss):
                break

            # タイムアウト処理
            if time.perf_counter() - start > 5.0:
                os.chdir(cd)
                return page - 1

        # 前回保存と同じ内容なら最終ページ(終了)
        if prev_saved is not None and np.array_equal(prev_saved, ss):
            os.chdir(cd)
            return page - 1

        # 画像保存と次ページへ
        cv2.imwrite(filename, ss)
        prev_saved = ss
        old = ss
        print(f'Page: {page}, {ss.shape}, {time.perf_counter() - start:.2f} sec')
        page += 1
        pag.press(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

    # ダイアログ後にKindleウィンドウを再フォーカス
    setup_kindle_window(hwnd)
    pag.moveTo(sc_w - 200, sc_h - 1)
    time.sleep(1)

    # Kindleウィンドウの矩形を取得してキャプチャ範囲を設定
    GetWindowRect = windll.user32.GetWindowRect
    rect = RECT()
    GetWindowRect(hwnd, pointer(rect))
    bbox = (rect.left, rect.top, rect.right, rect.bottom)

    # キャプチャを実行
    total_pages = capture_and_save_pages(bbox, title)

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

if __name__ == "__main__":
    main()

実行結果

実行後、指定フォルダ配下に連番の PNG ファイルが保存されました。

  • 001.png
  • 002.png
  • 003.png
  • ...
  • image.png

※同じページをスクショ撮ったりしてるので被ったものは削除しました。

この時点で、ページ単位の画像としては目的を達成できました。


PNG を PDF にした方法

PNG 化までは Python で行い、その後の PDF 化は以下の Web サービスを使いました。

保存された PNG ファイルをまとめてアップロードし、PDF に変換しました。
今回は手早く PDF にまとめたかったため、この方法にしました。


やってみて分かったこと

今回一番大きかったのは、ページ送りとキャプチャのタイミング調整でした。

単純にキー送信してすぐキャプチャすると、

  • 同じ画面を連続で拾う
  • ページ切り替え途中の画像を拾う
  • 最終ページ判定が不安定になる

といった問題が起きやすかったです。

そのため、

  • Kindle ウィンドウを確実に前面化する
  • ウィンドウ範囲だけをキャプチャする
  • 前フレームとの差分でページ切り替え完了を待つ
  • 同一画像で終了判定する

という流れにしたことで、かなり安定しました。


今後の改善点

今回は PNG 生成までは Python、PDF 化は Web サービスに分けました。
今後やるなら、以下も試したいです。

  • PNG → PDF まで Python で一括化する
  • 余白トリミングを追加する
  • 保存済み画像の重複チェックをもう少し賢くする

まとめ

参考記事をもとに試したコードは、そのままでは自分の環境で安定動作しませんでした。
ただ、ウィンドウ制御やキャプチャ範囲、ページ切り替え判定を見直すことで、PNG の連続保存までは動かせました。

また、Kindle のデータによっては page_change_keyleftright で切り替える必要があり、この点も実際に試してみて分かったポイントでした。

最終的には、生成した PNG を pdf.io で PDF 化して目的を達成できました。

同じように「サンプルコードがそのまま動かない」というケースでは、
フォーカス制御・待機時間・ページ送り方向・終了条件 を見直すと改善できることが多いと思います.

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?