質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.59%

Python3 Tkinter exe化後のPDFの画像変換ができない

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 501

person

score 103

環境:Windows10

PDFファイルから画像を生成するプログラムを作りました。
しかし、exe化したところ画像が生成されません。

エラー発生条件
・exe化するときのオプションに --noconsole --onefile の両方を付ける

オプションに --onefile のように片方のオプションを付けても問題なく意図した動作になりますが、--noconsole --onefile のように 両方のオプションを付けると
Failed to execute script main
ウィンドウが出てしまいます。

(PDFファイルはexeファイルと同ディレクトリに入れてます)

前回はソースコード上に

__file__


の記述があるとexeで取得できない指摘を受け、コードを書き換えたのですが上手くできませんでした。

該当のソースコード

from pdf2image import convert_from_path
import os
import sys

pdf = os.path.dirname(sys.argv[0]) + "/" + "test.pdf"

images = convert_from_path(pdf)
i = 0
for image in images:
    image.save(os.path.dirname(sys.argv[0]) + "/" + "{}.png".format(i), "png")
    i += 1

    # PDFのページ数が複数あっても1ページ目のみPNG変換
    # PDFの全ページをPNG変換するには次のif文を無効化する
    if i == 1:
        break

追記

エラー(sys.stderr = open('stderr.txt', 'w') で確認)

Traceback (most recent call last):
  File "site-packages\pdf2image\pdf2image.py", line 409, in pdfinfo_from_path
  File "subprocess.py", line 804, in __init__
  File "subprocess.py", line 1142, in _get_handles
OSError: [WinError 6] ハンドルが無効です。

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\ユーザ名\Desktop\test.py", line 9, in <module>
    images = convert_from_path(pdf)
  File "site-packages\pdf2image\pdf2image.py", line 89, in convert_from_path
  File "site-packages\pdf2image\pdf2image.py", line 430, in pdfinfo_from_path
pdf2image.exceptions.PDFInfoNotInstalledError: Unable to get page count. Is poppler installed and in PATH?

システム環境変数 にパスは追加してあるはず・・・。
再起動済み

popplerパス
popplerパス設定

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • argparse

    2020/04/18 19:31

    スクリプトの前半で `sys.stderr = open('stderr.txt', 'w')` 等として、エラー出力をファイルに得た場合、問題が発生する条件では何か情報は得られますでしょうか。

    `pdf2image` は Poppler コマンドを呼び出すライブラリなので、以下の issue の事例と同様、 `subprocess` が標準入出力のハンドルを得るのに失敗していることが原因の可能性があるかと存じます。
    https://github.com/pyinstaller/pyinstaller/issues/1339

    キャンセル

  • person

    2020/04/20 08:45

    質問文に追記しました。
    パスを通したつもりです。しかし、パスを通していないようなエラーが表示されました。

    キャンセル

回答 1

checkベストアンサー

0

「ハンドルが無効です」というエラーの示す通り、原因はパスではなく、 標準入力のハンドルを取得できず、 subprocess による子プロセスの起動に失敗してしまう ことにあります。

詳細は追記・修正依頼にて述べた 公式の GitHub issue や そこから派生した Wiki に記載が御座いますが、 PyInstaller で --noconsole (--windowed) かつ--onefile のファイルを作成した場合、 明示的に標準入出力のリダイレクト指定を与えない限り、 subprocess.Popen() が OSError "[Error 6] the handle is invalid." によって失敗してしまう という問題が知られています。

このため、同様の条件で pdf2image を使おうとすると、 このあたり や このあたり や このあたり にある、 stdin のリダイレクト指定が無い subprocess.Popen() の実行に失敗し、 Poppler を正しく呼び出すことが出来ません。

対策としては、前掲の Wiki にある通り、 subprocess.Popen() の stdin 引数を明示的に指定 すれば良いので、 pdf2image.py を直接編集して該当箇所を修正するか、あるいは次のようなコードによってモンキーパッチを当て、 stdin 引数を無理やり与えてしまうことが有効かと存じます。

import subprocess

_original_constructor = subprocess.Popen.__init__

def _patched_constructor(*args, **kwargs):
    kwargs['stdin'] = subprocess.PIPE

    return _original_constructor(*args, **kwargs)

subprocess.Popen.__init__ = _patched_constructor

原因の詳細

この問題をより正確に言うと、 PyInstaller で --noconsole かつ --onefile で作成したファイルを実行すると、 何故か GetStdHandle() API が INVALID_HANDLE_VALUE を返す  ため、 subprocess の この個所 にある、現在のプロセスの標準入出力ハンドルを取得しようとするデフォルトの処理が失敗し、例外を生じてしまうというものです。

INVALID_HANDLE_VALUE は上記のドキュメントの通り、 GetStdHandle() の処理に 失敗した場合に返る値 ですから、 このように _winapi モジュールが例外を生じる実装である 事には問題がありませんが、 そもそもなぜ GetStdHandle() が INVALID_HANDLE_VALUE を返すのか という点には疑問が残ります。確かに、 --noconsole を付与され、コンソールを持たないアプリケーションとしてビルドされたファイルは標準入出力を持たないかもしれませんが、GetStdHandle() のドキュメントでは 特に標準入出力ハンドルを持たない場合、 NULL を返す と書かれており、それ自体は失敗の理由にならず、他に失敗しそうな要因も見当たらないからです。 --onefile の有無が影響するというのも不思議です。

というわけで調べてみたのですが、どうもこれ、 PyInstaller の bootloader が持つ実装のバグ のようでした。

PyInstaller は --onefile 付きでビルドされたファイルを実行すると、次のような流れで 親子の 2 プロセスに分かれて 動作します。一つのファイルに諸々の必要なファイルを pack しているため、まずそれを展開する作業が必要になるという事ですね。コードとしては この辺り が該当します。

  1. 親プロセスはまず、自身が持つ各種ファイルを、一時ディレクトリに展開する
  2. 展開した一時ディレクトリを用いて子プロセスを生成する
  3. 子プロセスは実際の Python スクリプト処理を実行する
  4. 子プロセスの終了後、親プロセスは一時ディレクトリを清掃する

このうち、 2 の処理を行っているのが この pyi_utils_create_child() 関数 なのですが、 この個所 で、 STARTUPINFO 構造体を用いて、 生成しようとしている子プロセスに _get_osfhandle() API で取得した自プロセスの標準入出力ハンドルを渡そうとしている ことが分かります。

それ自体はまあ、子にも親と同じ環境で動作させるという意味で問題はないのですが、ここでさらに --noconsole が付き、標準入出力ハンドルを持っていないとなると話が変わってきます。ドキュメントにある通り、  _get_osfhandle() は与えられたファイルディスクリプタが無効な場合、 NULL ではなく INVALID_HANDLE_VALUE を返す ため、子プロセスが標準入出力として INVALID_HANDLE_VALUE を渡されて生成されてしまうからです。どうやらこの結果、「何故か GetStdHandle() が INVALID_HANDLE_VALUE を返してしまう」状況が発生し、今回のような subprocess.Popen() の不具合に繋がっている模様です。

従って、より根本的にこの問題を修正するのであれば、上記の個所で --noconsole の場合には標準入出力ハンドルを渡さない実装へと修正し、 bootloader をビルドしなおす というのが正しい対応のように思われます。さすがにそこまでやるというのは面倒ですが。

因みに、繰り返し述べている通り、 GetStdHandle() が INVALID_HANDLE_VALUE を返す ことが問題なので、次のように SetStdHandle() API を用い、とりあえず自身の標準入力ハンドルを NULL で上書きしてしまう ことでも、今回の問題は回避可能です。標準ストリームのハンドルが異常値となることで不具合を招く可能性は、 subprocess 以外のモジュールでも考えられるため、万が一そのような罠を踏んでしまった場合は、こちらの方がより汎用的で楽な解決方法かもしれません。

import ctypes
import _winapi

ctypes.windll.kernel32.SetStdHandle(_winapi.STD_INPUT_HANDLE, 0)

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.59%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る