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

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

新規登録して質問してみよう
ただいま回答率
85.48%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Q&A

解決済

1回答

6171閲覧

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

person

総合スコア223

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

0グッド

0クリップ

投稿2020/04/16 23:45

編集2020/04/20 00:13

環境:Windows10

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

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

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

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

前回はソースコード上に

__file__

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

該当のソースコード

Python

1from pdf2image import convert_from_path 2import os 3import sys 4 5pdf = os.path.dirname(sys.argv[0]) + "/" + "test.pdf" 6 7images = convert_from_path(pdf) 8i = 0 9for image in images: 10 image.save(os.path.dirname(sys.argv[0]) + "/" + "{}.png".format(i), "png") 11 i += 1 12 13 # PDFのページ数が複数あっても1ページ目のみPNG変換 14 # PDFの全ページをPNG変換するには次のif文を無効化する 15 if i == 1: 16 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パス設定

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

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

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

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

argparse

2020/04/18 10:31

スクリプトの前半で `sys.stderr = open('stderr.txt', 'w')` 等として、エラー出力をファイルに得た場合、問題が発生する条件では何か情報は得られますでしょうか。 `pdf2image` は Poppler コマンドを呼び出すライブラリなので、以下の issue の事例と同様、 `subprocess` が標準入出力のハンドルを得るのに失敗していることが原因の可能性があるかと存じます。 https://github.com/pyinstaller/pyinstaller/issues/1339
person

2020/04/19 23:45

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

回答1

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 引数を無理やり与えてしまうことが有効かと存じます。

python

1import subprocess 2 3_original_constructor = subprocess.Popen.__init__ 4 5def _patched_constructor(*args, **kwargs): 6 kwargs['stdin'] = subprocess.PIPE 7 8 return _original_constructor(*args, **kwargs) 9 10subprocess.Popen.__init__ = _patched_constructor

原因の詳細

この問題をより正確に言うと、 PyInstaller で --noconsole かつ --onefile で作成したファイルを実行すると、 何故か GetStdHandle() APIINVALID_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 以外のモジュールでも考えられるため、万が一そのような罠を踏んでしまった場合は、こちらの方がより汎用的で楽な解決方法かもしれません。

python

1import ctypes 2import _winapi 3 4ctypes.windll.kernel32.SetStdHandle(_winapi.STD_INPUT_HANDLE, 0)

投稿2020/04/20 16:44

argparse

総合スコア1017

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問