📘

StreamlitのUploadedFile.read()で、Document stream is emptyエラーにハマった

2023/12/08に公開

はじめに

初めまして。
都内IT企業で、データアルゴリズムチームのエンジニアをしております、Noraです。

今回の記事では、PythonでWeb開発を行えるフレームワーク「streamlit」において、PDFを読み込んで画像に変換する際にハマってしまったので、ハマりポイントとその解消法を解説いたします。

3分あれば読めるかと思いますので、同じポイントでハマった方はぜひご覧ください!

ハマったポイント

今回ハマったポイントは「Document stream is empty」というエラーです。

以下は、エラーが起こったスクリプトです。
index.pyファイルの、file = convert_streamlit_pdf_to_images(file)部分でエラー文が表示されます。

index.py
import streamlit as st
import pathlib
import sys

sys.path.append(str(pathlib.Path().absolute()))

from pdf_to_image import (
    convert_streamlit_pdf_to_images,
)


def index_page() -> None:
    st.title("タイトル")

    st.sidebar.markdown("### 読み込むPDFをアップロード(複数ファイル可)")
    upload_files = st.sidebar.file_uploader(
        "Upload a PDF or jpeg file", type=["pdf", "jpg"], accept_multiple_files=True
    )
    for file in upload_files:
        # ファイルがpdfなら、画像にする
        if file.type == "application/pdf":
            file = convert_streamlit_pdf_to_images(file) # ここでエラーが起こる
        st.image(file)


index_page()

次に、convert_streamlit_pdf_to_imagesメソッドの実装部分を見ます。
エラーとしては、images = convert_from_path(temp_file_path)部分で、PDFPageCountErrorがraiseされます。

pdf_to_image.py
from pathlib import Path
from pdf2image import convert_from_path
from streamlit.runtime.uploaded_file_manager import UploadedFile
from PIL import Image
import tempfile

def convert_streamlit_pdf_to_images(pdf_file: UploadedFile) -> list[Image.Image]:
    # ディスクに一時ファイルを書き出すことで、メモリ消費を抑える
    print(pdf_file.read())
    with tempfile.NamedTemporaryFile() as temp_file:
        temp_file.write(pdf_file.read())
        temp_file_path = temp_file.name

        images = convert_from_path(temp_file_path)
    return images

エラー内容は以下です。

PDFPageCountError: Unable to get page count. Syntax Error: Document stream is empty
Traceback:
File "/Users/apple/src/quackshift/contract-ocr/.venv/lib/python3.11/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 534, in _run_script
    exec(code, module.__dict__)
File "/Users/apple/src/quackshift/contract-ocr/src/frontend/pages/index.py", line 29, in <module>
    index_page()
File "/Users/apple/src/quackshift/contract-ocr/src/frontend/pages/index.py", line 22, in index_page
    file = convert_streamlit_pdf_to_images(file)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/apple/src/quackshift/contract-ocr/src/backend/modules/pdf_to_image.py", line 27, in convert_streamlit_pdf_to_images
    images = convert_from_path(temp_file_path)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/apple/src/quackshift/contract-ocr/.venv/lib/python3.11/site-packages/pdf2image/pdf2image.py", line 127, in convert_from_path
    page_count = pdfinfo_from_path(
                 ^^^^^^^^^^^^^^^^^^
File "/Users/apple/src/quackshift/contract-ocr/.venv/lib/python3.11/site-packages/pdf2image/pdf2image.py", line 598, in pdfinfo_from_path
    raise PDFPageCountError(

エラーの起こった原因

まず「Document stream is empty」というのは、PDFドキュメントが空であると認識されたことによるエラーです。
一方で、print(pdf_file.read())で、プリントデバッグした際には、適切にバイト列が表示されるため、PDFが空ではなさそうです。

結論、原因は*pdf_file.read()を2回呼び出していることにありました。
1回目のpdf_file.read()で、ファイルの内容を読み込んだ後にファイルポインタが最後まで移動してしまいます。
そのため、2回目のpdf_file.read()では、ファイルポインタが最後にあり、ファイルが空に見えてしまうようでした。

解消法

解消法としては、pdf_file.read()を1回だけ呼び出すように修正すると、解消されました🎉

以下のようにpdf_to_image.pyファイルを修正します。

pdf_to_image.py
from pathlib import Path
from pdf2image import convert_from_path
from streamlit.runtime.uploaded_file_manager import UploadedFile
from PIL import Image
import tempfile

def convert_streamlit_pdf_to_images(pdf_file: UploadedFile) -> list[Image.Image]:
    # ディスクに一時ファイルを書き出すことで、メモリ消費を抑える
    with tempfile.NamedTemporaryFile() as temp_file:
        temp_file.write(pdf_file.read())
        temp_file_path = temp_file.name

        images = convert_from_path(temp_file_path)
    return images

あと書き

記事をお読みいただき、ありがとうございました。
今回はStreamlitプロジェクトにおける、pdfファイルの読み込みで起こったエラーについて解説してきました。

シンプルなミスでしたが、もし同じ内容でハマった方に役立つことができれば、とても嬉しく思います。

今後も皆さんに有益な情報を届けたいと思っております。

もし補足点・修正等ございましたら、ご気軽にコメントいただけますと幸いです。
では、失礼いたします!

Discussion