Dify × PDF:スキャンPDFをAIに読ませるためのシンプル解決策
作ったもの
DifyのプラグインPDF to Images Converter Pluginを作成しました!
晴れて公式マーケットプレイスに登録されましたので、プラグインの紹介とマーケットページには書ききれない従来の問題点の説明のために記事を作成します。
はじめに
DifyでPDFを読み込みAIモデルに内容を渡して何かしらの処理を行うシーンで活用するプラグインです。
その際PDFは大きくは以下の2種類に大きく分かれます。
(pythonのライブラリを使う場合など、Difyに限らずシステム・プログラムで扱う場合はこの点のケアが必要になると考えています。)
-
テキスト埋め込みPDF
PDF内部に文字情報が埋め込まれているもの。要するにマウスでテキストをコピーできるものですね。
(下記の「Write for yourself」の部分のようにテキストを選択できる。)

「Zennとは」を「Zennとは?.pdf」で保存したもの -
スキャンPDF(画像ベースPDF、非テキスト埋め込みPDF、など)
PDF内部に文字情報はなく、ページ全体が画像として格納されているPDFです。このケースでは文字を読み取れないため、OCRや画像認識が必要になります。
(正式名称がAIに聞いてもわからなかったので「スキャンPDF」と呼びます。)
従来の問題点
解決策の前に、それぞれのパターンでどのようになるのか、問題点を共有させてください。
DifyでPDFを処理する際は「ファイルアップロード」[1]機能と「テキスト抽出」[2]ノードを使うのが仕様に沿った使い方になります。
テキスト埋め込みPDFの場合は標準機能でOK
以下は「Zennとは?.pdf」を読み取る簡単なDifyのフローです。
真ん中下段の「text」の部分の通り、pdfの内容を読み取れていることが確認できます。

もしもこんな最小構成でも使いたい方がいましたら以下をインポートしてご利用ください。
test.yml
app:
description: ''
icon: 🤖
icon_background: '#FFEAD5'
mode: advanced-chat
name: test
use_icon_as_answer_icon: false
dependencies:
- current_identifier: null
type: marketplace
value:
marketplace_plugin_unique_identifier: langgenius/openai:0.2.3@5a7f82fa86e28332ad51941d0b491c1e8a38ead539656442f7bf4c6129cd15fa
kind: app
version: 0.3.1
workflow:
conversation_variables: []
environment_variables: []
features:
file_upload:
allowed_file_extensions:
- .JPG
- .JPEG
- .PNG
- .GIF
- .WEBP
- .SVG
allowed_file_types:
- image
- document
allowed_file_upload_methods:
- local_file
enabled: true
fileUploadConfig:
audio_file_size_limit: 50
batch_count_limit: 5
file_size_limit: 15
image_file_size_limit: 10
video_file_size_limit: 100
workflow_file_upload_limit: 10
image:
enabled: false
number_limits: 3
transfer_methods:
- local_file
- remote_url
number_limits: 3
opening_statement: ''
retriever_resource:
enabled: true
sensitive_word_avoidance:
enabled: false
speech_to_text:
enabled: false
suggested_questions: []
suggested_questions_after_answer:
enabled: false
text_to_speech:
enabled: false
language: ''
voice: ''
graph:
edges:
- data:
isInIteration: false
isInLoop: false
sourceType: start
targetType: document-extractor
id: 1754876217921-source-1756044569689-target
source: '1754876217921'
sourceHandle: source
target: '1756044569689'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: document-extractor
targetType: llm
id: 1756044569689-source-1756044579087-target
source: '1756044569689'
sourceHandle: source
target: '1756044579087'
targetHandle: target
type: custom
zIndex: 0
- data:
isInIteration: false
isInLoop: false
sourceType: llm
targetType: answer
id: 1756044579087-source-1756044632703-target
source: '1756044579087'
sourceHandle: source
target: '1756044632703'
targetHandle: target
type: custom
zIndex: 0
nodes:
- data:
desc: ''
selected: false
title: 開始
type: start
variables: []
height: 54
id: '1754876217921'
position:
x: 429.3783617376292
y: 23.778184703671002
positionAbsolute:
x: 429.3783617376292
y: 23.778184703671002
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
desc: ''
is_array_file: true
selected: false
title: テキスト抽出
type: document-extractor
variable_selector:
- sys
- files
height: 94
id: '1756044569689'
position:
x: 476.8571428571429
y: 105.35714285714283
positionAbsolute:
x: 476.8571428571429
y: 105.35714285714283
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
context:
enabled: false
variable_selector: []
desc: ''
model:
completion_params:
temperature: 0.7
mode: chat
name: gpt-4o-mini
provider: langgenius/openai/openai
prompt_template:
- id: 4a44c71b-bde2-4500-998d-7a159fb9c46d
role: system
text: ''
- id: 949bf395-1a93-4c3c-b7c8-58040033ede2
role: user
text: '{{#sys.query#}}
{{#1756044569689.text#}}'
selected: false
title: LLM
type: llm
variables: []
vision:
enabled: false
height: 90
id: '1756044579087'
position:
x: 554.4313111598581
y: 225.6386357299544
positionAbsolute:
x: 554.4313111598581
y: 225.6386357299544
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
- data:
answer: '{{#1756044579087.text#}}'
desc: ''
selected: false
title: 回答
type: answer
variables: []
height: 105
id: '1756044632703'
position:
x: 609.7149753261024
y: 338.058419526812
positionAbsolute:
x: 609.7149753261024
y: 338.058419526812
selected: false
sourcePosition: right
targetPosition: left
type: custom
width: 244
viewport:
x: -451.4914715123374
y: 204.4258469818957
zoom: 1.0051611574364074
スキャンPDFの場合は標準機能で読み取れない
一方スキャンタイプで同じページを保存した「Zennとは?_scan.pdf」では「text」が空欄になってしまい、情報を取得できていないことがわかります。テキスト抽出の仕様上、これは仕方のないことです。

スキャンPDFの内容。こちらはテキストをコピーできない

スキャンPDFの内容をテキスト抽出ノードで抽出できない
解決策
私が作成したDifyのプラグインPDF to Images Converter Pluginを使った様子がこちらです。
スキャンPDFの「Zennとは?_scan.pdf」の内容を画像としてAI(LLMノード)に渡しており、しっかりと内容を答えてくれています。

画像変換後の「Zennとは?_scan_page_1.png」がAIへの入力に使われている
AI処理用の「LLM」ノードでは、「ビジョン」欄にプラグインが作成した画像ファイルを指定しています。こうすることで開発者側が画像からテキスト情報を取り出すことなく、AI自身のOCR機能を利用して読み取らせることが出来ています。

設定の下部に「(x)pdf変換ツール/(x)files Array[Files]」の形で設定している
使い方
マーケットからインストール出来るので以下の手順で簡単導入可能です。
- Difyクラウド版にログイン
- プラグインマーケットから pdf-to-images をインストール
- 以下画像やymlファイルを参考にワークフローを作成

Githubの画像
フロー作成後に以下のファイルをインポートすることで、Githubの画像のようなノード配置を簡単にできますのでお試しください!
こちらは解決策で見せたものから一歩進んで、画像でもテキスト埋め込みPDFでもスキャンPDFでも、何が来てもAIが内容を読み取って処理できるフローになっています!
まとめ
- PDFは「テキスト埋め込みPDF」と「スキャンPDF」に分かれる ※自分の勝手な呼称
- テキスト埋め込みPDFは「テキスト抽出ノード」で簡単に処理可能
- スキャンPDFは pdf-to-imagesプラグイン + LLMのvision機能 でOCR代替
これで、どんなPDFをアップロードしてもAIが内容を読み取ることができます!🥳 🙌
Discussion