2025-01-21 20:52:28 +08:00
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
2024-08-15 09:17:36 +08:00
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
2025-01-21 20:52:28 +08:00
2024-11-14 17:13:48 +08:00
import logging
2025-03-13 18:48:32 +08:00
import re
2025-10-17 18:46:47 +08:00
import os
2025-03-13 18:48:32 +08:00
from functools import reduce
2024-08-15 09:17:36 +08:00
from io import BytesIO
from timeit import default_timer as timer
2025-03-13 18:48:32 +08:00
from docx import Document
2025-08-14 12:14:03 +08:00
from docx . opc . pkgreader import _SerializedRelationships , _SerializedRelationship
2026-01-07 15:08:17 +08:00
from docx . table import Table as DocxTable
from docx . text . paragraph import Paragraph
2025-08-14 12:14:03 +08:00
from docx . opc . oxml import parse_xml
2025-07-15 13:03:01 +08:00
from markdown import markdown
2025-03-13 18:48:32 +08:00
from PIL import Image
2025-11-25 19:54:06 +08:00
from common . token_utils import num_tokens_from_string
2025-03-13 18:48:32 +08:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
from common . constants import LLMType , MAXIMUM_PAGE_NUMBER
2025-03-18 14:52:20 +08:00
from api . db . services . llm_service import LLMBundle
2026-06-10 13:04:13 +08:00
from api . db . joint_services . tenant_model_service import (
ensure_mineru_from_env ,
2026-06-10 17:44:50 +08:00
ensure_opendataloader_from_env ,
2026-06-10 13:04:13 +08:00
ensure_paddleocr_from_env ,
get_first_provider_model_name ,
get_model_config_from_provider_instance ,
get_tenant_default_model_by_type ,
)
2025-11-03 19:26:09 +08:00
from rag . utils . file_utils import extract_embed_file , extract_links_from_pdf , extract_links_from_docx , extract_html
2026-03-17 15:14:06 +03:00
from deepdoc . parser import DocxParser , EpubParser , ExcelParser , HtmlParser , JsonParser , MarkdownElementExtractor , MarkdownParser , PdfParser , TxtParser
2026-01-09 17:48:45 +08:00
from deepdoc . parser . figure_parser import VisionFigureParser , vision_figure_parser_docx_wrapper_naive , vision_figure_parser_pdf_wrapper
2025-03-20 10:48:38 +08:00
from deepdoc . parser . pdf_parser import PlainParser , VisionParser
2025-10-23 19:44:25 +08:00
from deepdoc . parser . docling_parser import DoclingParser
2025-10-27 15:14:58 +08:00
from deepdoc . parser . tcadp_parser import TCADPParser
2026-01-27 12:43:01 +08:00
from common . float_utils import normalize_overlapped_percent
2025-12-17 19:48:24 +08:00
from common . parser_config_utils import normalize_layout_recognizer
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
from common . text_utils import normalize_arabic_presentation_forms
2026-01-09 17:48:45 +08:00
from rag . nlp import (
concat_img ,
find_codec ,
naive_merge ,
naive_merge_with_images ,
naive_merge_docx ,
rag_tokenizer ,
tokenize_chunks ,
doc_tokenize_chunks_with_images ,
tokenize_table ,
append_context2table_image4pdf ,
tokenize_chunks_with_images ,
) # noqa: F401
2026-06-03 10:49:28 +08:00
def _is_short_header ( text , max_tokens = 50 ) :
"""
Check if text is a short markdown header .
Args :
text : The text to check
max_tokens : Maximum tokens for a header to be considered " short "
Returns :
bool : True if text is a short markdown header , False otherwise
"""
if not text or not text . strip ( ) :
return False
# Check if it matches markdown header pattern: 1-6 # followed by space
if not re . match ( r " ^# { 1,6} \ s+ " , text . strip ( ) ) :
return False
# Check if token count is below threshold
return num_tokens_from_string ( text ) < max_tokens
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
def _normalize_section_text_for_rtl_presentation_forms ( sections ) :
if not sections :
return sections
normalized_sections = [ ]
for section in sections :
if isinstance ( section , tuple ) :
if not section :
normalized_sections . append ( section )
continue
text = section [ 0 ]
normalized_text = normalize_arabic_presentation_forms ( text )
normalized_sections . append ( ( normalized_text , * section [ 1 : ] ) )
continue
if isinstance ( section , list ) :
if not section :
normalized_sections . append ( section )
continue
text = section [ 0 ]
normalized_text = normalize_arabic_presentation_forms ( text )
normalized_sections . append ( [ normalized_text , * section [ 1 : ] ] )
continue
normalized_sections . append ( normalize_arabic_presentation_forms ( section ) )
return normalized_sections
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_deepdoc ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
callback = callback
binary = binary
pdf_parser = pdf_cls ( ) if pdf_cls else Pdf ( )
2026-01-09 17:48:45 +08:00
sections , tables = pdf_parser ( filename if not binary else binary , from_page = from_page , to_page = to_page , callback = callback )
2025-11-06 15:20:35 +08:00
2026-01-05 09:55:43 +08:00
tables = vision_figure_parser_pdf_wrapper (
tbls = tables ,
sections = sections ,
callback = callback ,
* * kwargs ,
)
2025-11-05 13:00:42 +08:00
return sections , tables , pdf_parser
2025-12-17 19:48:24 +08:00
def by_mineru (
2026-01-09 17:48:45 +08:00
filename ,
binary = None ,
from_page = 0 ,
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
to_page = MAXIMUM_PAGE_NUMBER ,
2026-01-09 17:48:45 +08:00
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
mineru_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
2025-12-17 19:48:24 +08:00
) :
2025-12-09 18:54:14 +08:00
pdf_parser = None
if tenant_id :
if not mineru_llm_name :
try :
2026-06-10 13:04:13 +08:00
mineru_llm_name = get_first_provider_model_name ( tenant_id , " MinerU " , LLMType . OCR ) or ensure_mineru_from_env ( tenant_id )
2025-12-09 18:54:14 +08:00
except Exception as e : # best-effort fallback
logging . warning ( f " fallback to env mineru: { e } " )
if mineru_llm_name :
try :
2026-05-29 17:39:41 +08:00
ocr_model_config = get_model_config_from_provider_instance ( tenant_id , LLMType . OCR , mineru_llm_name )
2026-03-05 17:27:17 +08:00
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
2025-12-09 18:54:14 +08:00
pdf_parser = ocr_model . mdl
Feat: VLM image descriptions in MinerU parser (#14869) (#14946)
## Summary
Closes #14869.
Adds VLM-based semantic descriptions to **image chunks produced by the
MinerU parser**, closing a long-standing parity gap with the deepdoc
parser's `VisionFigureParser`. A maintainer flagged this in #13342
("We may add the VLM enhancement to MinerU parser as well") and an
earlier proposal exists in #13824; this PR lands the change end-to-end
inside the existing parser plumbing.
## Why
Today the MinerU parser returns image chunks containing only the
native `image_caption` and `image_footnote` strings from MinerU's
JSON. When neither is present (or when both are sparse), the chunk
carries effectively no searchable content for the figure and
retrieval misses it entirely. Users who configured a local VLM
(reporter's case: Gemma-4-31B) had to post-process MinerU's
`tmp/*.json` themselves.
The deepdoc parser already solves this via
[`VisionFigureParser`](deepdoc/parser/figure_parser.py): when the
tenant has an `IMAGE2TEXT` model configured, each figure gets a
semantic description merged into its chunk. This PR brings the same
behavior to MinerU.
## What changed
### `deepdoc/parser/mineru_parser.py`
- **New method `_enhance_images_with_vlm(outputs, vision_model,
callback=None)`** —
collects every `IMAGE` block with a readable `img_path`, runs
`rag.app.picture.vision_llm_chunk` in a 10-worker
`ThreadPoolExecutor` using the existing
`vision_llm_figure_describe_prompt`, and writes the result back as
`vlm_description`. Per-image failures are logged and skipped — they
never abort the run.
- **`_transfer_to_sections` (IMAGE branch)** — folds
`vlm_description` into the section text alongside caption +
footnote, so the description becomes part of the chunk and is
searchable / retrievable.
- **`parse_pdf`** — after `_read_output`, calls
`_enhance_images_with_vlm(outputs, vision_model, callback=callback)`
when a `vision_model` kwarg is supplied. Wrapped in `try / except`
so a VLM outage cannot break parsing.
### `rag/app/naive.py` (`by_mineru`)
After successfully resolving the MinerU OCR parser, also resolves the
tenant's default `LLMType.IMAGE2TEXT` model via
`get_tenant_default_model_by_type`, wraps it in an `LLMBundle`, and
injects it as `kwargs["vision_model"]` before delegating to
`parse_pdf`.
## Behavior
| Tenant config | Behavior |
|---|---|
| `IMAGE2TEXT` model configured | MinerU image chunks contain `caption +
footnote + VLM description`. Retrieval against figures now actually
works. |
| No `IMAGE2TEXT` model configured | Exact same output as today (caption
+ footnote only). Lookup fails silently with an info log; no error, no
regression. |
| VLM call fails for a single image | That image silently falls back to
caption + footnote; other images proceed. |
| Caller already passes `vision_model` in kwargs | We don't override it
— `if "vision_model" not in kwargs` guards the lookup. |
## Files
- `deepdoc/parser/mineru_parser.py` (+56)
- `rag/app/naive.py` (+13)
2026-05-19 01:08:10 -07:00
# Closes #14869: when the tenant has an IMAGE2TEXT model
# configured, let the MinerU parser enrich image chunks with
# VLM-generated semantic descriptions (parity with deepdoc's
# VisionFigureParser). Best-effort — fall back silently if
# no vision model is available.
if " vision_model " not in kwargs :
try :
vision_model_config = get_tenant_default_model_by_type ( tenant_id , LLMType . IMAGE2TEXT )
kwargs [ " vision_model " ] = LLMBundle ( tenant_id = tenant_id , model_config = vision_model_config , lang = lang )
except Exception as vlm_err :
logging . info ( f " [MinerU] no IMAGE2TEXT model for tenant; skipping image VLM enhancement: { vlm_err } " )
2025-12-09 18:54:14 +08:00
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
2025-12-16 07:15:25 +02:00
lang = lang ,
2025-12-17 19:48:24 +08:00
* * kwargs ,
2025-12-09 18:54:14 +08:00
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle MinerU ( { mineru_llm_name } ): { e } " )
2025-11-05 13:00:42 +08:00
2025-12-09 18:54:14 +08:00
if callback :
callback ( - 1 , " MinerU not found. " )
return None , None , None
2025-11-05 13:00:42 +08:00
2025-12-16 07:15:25 +02:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_docling ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
pdf_parser = DoclingParser ( )
2025-11-20 19:07:17 +08:00
parse_method = kwargs . get ( " parse_method " , " raw " )
2025-11-05 13:00:42 +08:00
if not pdf_parser . check_installation ( ) :
2026-03-12 18:09:03 +09:00
if callback :
callback ( - 1 , " Docling not found. " )
2025-11-10 15:08:24 +08:00
return None , None , pdf_parser
2025-11-05 13:00:42 +08:00
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
2026-03-12 18:09:03 +09:00
output_dir = os . environ . get ( " DOCLING_OUTPUT_DIR " , " " ) ,
delete_output = bool ( int ( os . environ . get ( " DOCLING_DELETE_OUTPUT " , 1 ) ) ) ,
docling_server_url = os . environ . get ( " DOCLING_SERVER_URL " , " " ) ,
2026-01-09 17:48:45 +08:00
parse_method = parse_method ,
2025-11-05 13:00:42 +08:00
)
return sections , tables , pdf_parser
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
def by_opendataloader (
filename ,
binary = None ,
from_page = 0 ,
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
to_page = MAXIMUM_PAGE_NUMBER ,
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
opendataloader_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
) :
if tenant_id :
if not opendataloader_llm_name :
try :
2026-06-10 17:44:50 +08:00
opendataloader_llm_name = get_first_provider_model_name ( tenant_id , " OpenDataLoader " , LLMType . OCR ) or ensure_opendataloader_from_env ( tenant_id )
except Exception as e : # best-effort fallback
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
logging . warning ( f " fallback to env opendataloader: { e } " )
if opendataloader_llm_name :
try :
2026-05-29 17:39:41 +08:00
ocr_model_config = get_model_config_from_provider_instance ( tenant_id , LLMType . OCR , opendataloader_llm_name )
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
pdf_parser = ocr_model . mdl
2026-05-06 15:00:55 +08:00
parse_options = { k : kwargs [ k ] for k in ( " hybrid " , " image_output " , " sanitize " ) if k in kwargs }
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
2026-05-06 15:00:55 +08:00
* * parse_options ,
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle OpenDataLoader ( { opendataloader_llm_name } ): { e } " )
if callback :
callback ( - 1 , " OpenDataLoader not found. " )
return None , None , None
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_tcadp ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
tcadp_parser = TCADPParser ( )
if not tcadp_parser . check_installation ( ) :
callback ( - 1 , " TCADP parser not available. Please check Tencent Cloud API configuration. " )
2025-11-10 15:08:24 +08:00
return None , None , tcadp_parser
2025-11-05 13:00:42 +08:00
2026-01-09 17:48:45 +08:00
sections , tables = tcadp_parser . parse_pdf ( filepath = filename , binary = binary , callback = callback , output_dir = os . environ . get ( " TCADP_OUTPUT_DIR " , " " ) , file_type = " PDF " )
2025-11-05 13:00:42 +08:00
return sections , tables , tcadp_parser
2026-01-09 17:48:45 +08:00
def by_paddleocr (
filename ,
binary = None ,
from_page = 0 ,
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
to_page = MAXIMUM_PAGE_NUMBER ,
2026-01-09 17:48:45 +08:00
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
paddleocr_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
) :
pdf_parser = None
if tenant_id :
if not paddleocr_llm_name :
try :
2026-06-10 13:04:13 +08:00
paddleocr_llm_name = get_first_provider_model_name ( tenant_id , " PaddleOCR " , LLMType . OCR ) or ensure_paddleocr_from_env ( tenant_id )
2026-01-09 17:48:45 +08:00
except Exception as e : # best-effort fallback
logging . warning ( f " fallback to env paddleocr: { e } " )
if paddleocr_llm_name :
try :
2026-05-29 17:39:41 +08:00
ocr_model_config = get_model_config_from_provider_instance ( tenant_id , LLMType . OCR , paddleocr_llm_name )
2026-03-05 17:27:17 +08:00
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
2026-01-09 17:48:45 +08:00
pdf_parser = ocr_model . mdl
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
* * kwargs ,
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle PaddleOCR ( { paddleocr_llm_name } ): { e } " )
return None , None , None
if callback :
callback ( - 1 , " PaddleOCR not found. " )
return None , None , None
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def by_plaintext ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , callback = None , * * kwargs ) :
2025-12-17 19:48:24 +08:00
layout_recognizer = ( kwargs . get ( " layout_recognizer " ) or " " ) . strip ( )
if ( not layout_recognizer ) or ( layout_recognizer == " Plain Text " ) :
2025-11-05 13:00:42 +08:00
pdf_parser = PlainParser ( )
else :
2025-12-17 19:48:24 +08:00
tenant_id = kwargs . get ( " tenant_id " )
if not tenant_id :
raise ValueError ( " tenant_id is required when using vision layout recognizer " )
2026-05-29 17:39:41 +08:00
vision_model_config = get_model_config_from_provider_instance ( tenant_id , LLMType . IMAGE2TEXT , layout_recognizer )
2025-12-17 19:48:24 +08:00
vision_model = LLMBundle (
tenant_id ,
2026-03-05 17:27:17 +08:00
model_config = vision_model_config ,
2025-12-17 19:48:24 +08:00
lang = kwargs . get ( " lang " , " Chinese " ) ,
)
2025-11-05 13:00:42 +08:00
pdf_parser = VisionParser ( vision_model = vision_model , * * kwargs )
2025-11-20 10:08:42 +08:00
2026-01-09 17:48:45 +08:00
sections , tables = pdf_parser ( filename if not binary else binary , from_page = from_page , to_page = to_page , callback = callback )
2025-11-05 13:00:42 +08:00
return sections , tables , pdf_parser
PARSERS = {
2025-12-29 12:01:18 +08:00
" deepdoc " : by_deepdoc ,
" mineru " : by_mineru ,
" docling " : by_docling ,
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
" opendataloader " : by_opendataloader ,
2026-03-04 11:51:10 +08:00
" tcadp parser " : by_tcadp ,
2026-01-09 17:48:45 +08:00
" paddleocr " : by_paddleocr ,
2025-11-06 15:20:35 +08:00
" plaintext " : by_plaintext , # default
2025-11-05 13:00:42 +08:00
}
2024-09-30 16:59:39 +08:00
2024-08-15 09:17:36 +08:00
class Docx ( DocxParser ) :
def __init__ ( self ) :
pass
def __clean ( self , line ) :
line = re . sub ( r " \ u3000 " , " " , line ) . strip ( )
return line
2025-03-21 18:42:36 +08:00
def __get_nearest_title ( self , table_index , filename ) :
""" Get the hierarchical title structure before the table """
import re
from docx . text . paragraph import Paragraph
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
titles = [ ]
blocks = [ ]
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Get document name from filename parameter
doc_name = re . sub ( r " \ .[a-zA-Z]+$ " , " " , filename )
if not doc_name :
doc_name = " Untitled Document "
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Collect all document blocks while maintaining document order
try :
# Iterate through all paragraphs and tables in document order
for i , block in enumerate ( self . doc . _element . body ) :
2026-01-09 17:48:45 +08:00
if block . tag . endswith ( " p " ) : # Paragraph
2025-03-21 18:42:36 +08:00
p = Paragraph ( block , self . doc )
2026-01-09 17:48:45 +08:00
blocks . append ( ( " p " , i , p ) )
elif block . tag . endswith ( " tbl " ) : # Table
blocks . append ( ( " t " , i , None ) ) # Table object will be retrieved later
2025-03-21 18:42:36 +08:00
except Exception as e :
logging . error ( f " Error collecting blocks: { e } " )
return " "
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find the target table position
target_table_pos = - 1
table_count = 0
for i , ( block_type , pos , _ ) in enumerate ( blocks ) :
2026-01-09 17:48:45 +08:00
if block_type == " t " :
2025-03-21 18:42:36 +08:00
if table_count == table_index :
target_table_pos = pos
break
table_count + = 1
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if target_table_pos == - 1 :
return " " # Target table not found
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find the nearest heading paragraph in reverse order
nearest_title = None
2025-12-29 12:01:18 +08:00
for i in range ( len ( blocks ) - 1 , - 1 , - 1 ) :
2025-03-21 18:42:36 +08:00
block_type , pos , block = blocks [ i ]
if pos > = target_table_pos : # Skip blocks after the table
continue
2025-07-15 13:03:01 +08:00
2026-01-09 17:48:45 +08:00
if block_type != " p " :
2025-03-21 18:42:36 +08:00
continue
2025-07-15 13:03:01 +08:00
2025-07-01 17:38:11 +08:00
if block . style and block . style . name and re . search ( r " Heading \ s*( \ d+) " , block . style . name , re . I ) :
2025-03-21 18:42:36 +08:00
try :
level_match = re . search ( r " ( \ d+) " , block . style . name )
if level_match :
level = int ( level_match . group ( 1 ) )
if level < = 7 : # Support up to 7 heading levels
title_text = block . text . strip ( )
if title_text : # Avoid empty titles
nearest_title = ( level , title_text )
break
except Exception as e :
logging . error ( f " Error parsing heading level: { e } " )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if nearest_title :
# Add current title
titles . append ( nearest_title )
current_level = nearest_title [ 0 ]
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find all parent headings, allowing cross-level search
while current_level > 1 :
found = False
2025-12-29 12:01:18 +08:00
for i in range ( len ( blocks ) - 1 , - 1 , - 1 ) :
2025-03-21 18:42:36 +08:00
block_type , pos , block = blocks [ i ]
if pos > = target_table_pos : # Skip blocks after the table
continue
2025-07-15 13:03:01 +08:00
2026-01-09 17:48:45 +08:00
if block_type != " p " :
2025-03-21 18:42:36 +08:00
continue
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if block . style and re . search ( r " Heading \ s*( \ d+) " , block . style . name , re . I ) :
try :
level_match = re . search ( r " ( \ d+) " , block . style . name )
if level_match :
level = int ( level_match . group ( 1 ) )
# Find any heading with a higher level
2025-07-15 13:03:01 +08:00
if level < current_level :
2025-03-21 18:42:36 +08:00
title_text = block . text . strip ( )
if title_text : # Avoid empty titles
titles . append ( ( level , title_text ) )
current_level = level
found = True
break
except Exception as e :
logging . error ( f " Error parsing parent heading: { e } " )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if not found : # Break if no parent heading is found
break
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Sort by level (ascending, from highest to lowest)
titles . sort ( key = lambda x : x [ 0 ] )
# Organize titles (from highest to lowest)
hierarchy = [ doc_name ] + [ t [ 1 ] for t in titles ]
return " > " . join ( hierarchy )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
return " "
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def __call__ ( self , filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER ) :
2026-01-09 17:48:45 +08:00
self . doc = Document ( filename ) if not binary else Document ( BytesIO ( binary ) )
2024-08-15 09:17:36 +08:00
pn = 0
lines = [ ]
last_image = None
2026-01-07 15:08:17 +08:00
table_idx = 0
def flush_last_image ( ) :
nonlocal last_image , lines
if last_image is not None :
lines . append ( { " text " : " " , " image " : last_image , " table " : None , " style " : " Image " } )
last_image = None
for block in self . doc . _element . body :
2024-08-15 09:17:36 +08:00
if pn > to_page :
break
2026-01-07 15:08:17 +08:00
2026-01-09 17:48:45 +08:00
if block . tag . endswith ( " p " ) :
2026-01-07 15:08:17 +08:00
p = Paragraph ( block , self . doc )
if from_page < = pn < to_page :
text = p . text . strip ( )
style_name = p . style . name if p . style else " "
if text :
if style_name == " Caption " :
former_image = None
if lines and lines [ - 1 ] . get ( " image " ) and lines [ - 1 ] . get ( " style " ) != " Caption " :
former_image = lines [ - 1 ] . get ( " image " )
lines . pop ( )
elif last_image is not None :
former_image = last_image
last_image = None
lines . append (
{
" text " : self . __clean ( text ) ,
" image " : former_image if former_image else None ,
" table " : None ,
}
)
else :
flush_last_image ( )
lines . append (
{
" text " : self . __clean ( text ) ,
" image " : None ,
" table " : None ,
}
)
current_image = self . get_picture ( self . doc , p )
if current_image is not None :
lines . append (
{
" text " : " " ,
" image " : current_image ,
" table " : None ,
}
)
2024-08-15 09:17:36 +08:00
else :
current_image = self . get_picture ( self . doc , p )
2026-01-07 15:08:17 +08:00
if current_image is not None :
2024-08-15 09:17:36 +08:00
last_image = current_image
2026-01-07 15:08:17 +08:00
for run in p . runs :
xml = run . _element . xml
if " lastRenderedPageBreak " in xml :
pn + = 1
continue
if " w:br " in xml and ' type= " page " ' in xml :
pn + = 1
2026-01-09 17:48:45 +08:00
elif block . tag . endswith ( " tbl " ) :
2026-01-07 15:08:17 +08:00
if pn < from_page or pn > to_page :
table_idx + = 1
2024-08-15 09:17:36 +08:00
continue
2026-01-07 15:08:17 +08:00
flush_last_image ( )
tb = DocxTable ( block , self . doc )
title = self . __get_nearest_title ( table_idx , filename )
html = " <table> "
if title :
html + = f " <caption>Table Location: { title } </caption> "
for r in tb . rows :
html + = " <tr> "
col_idx = 0
try :
while col_idx < len ( r . cells ) :
span = 1
c = r . cells [ col_idx ]
for j in range ( col_idx + 1 , len ( r . cells ) ) :
if c . text == r . cells [ j ] . text :
span + = 1
col_idx = j
else :
break
col_idx + = 1
html + = f " <td> { c . text } </td> " if span == 1 else f " <td colspan= ' { span } ' > { c . text } </td> "
except Exception as e :
logging . warning ( f " Error parsing table, ignore: { e } " )
html + = " </tr> "
html + = " </table> "
lines . append ( { " text " : " " , " image " : None , " table " : html } )
table_idx + = 1
flush_last_image ( )
new_line = [ ( line . get ( " text " ) , line . get ( " image " ) , line . get ( " table " ) ) for line in lines ]
return new_line
2025-10-10 09:39:15 +08:00
def to_markdown ( self , filename = None , binary = None , inline_images : bool = True ) :
"""
This function uses mammoth , licensed under the BSD 2 - Clause License .
"""
import base64
import uuid
import mammoth
from markdownify import markdownify
docx_file = BytesIO ( binary ) if binary else open ( filename , " rb " )
def _convert_image_to_base64 ( image ) :
try :
with image . open ( ) as image_file :
image_bytes = image_file . read ( )
encoded = base64 . b64encode ( image_bytes ) . decode ( " utf-8 " )
base64_url = f " data: { image . content_type } ;base64, { encoded } "
alt_name = " image "
alt_name = f " img_ { uuid . uuid4 ( ) . hex [ : 8 ] } "
return { " src " : base64_url , " alt " : alt_name }
except Exception as e :
logging . warning ( f " Failed to convert image to base64: { e } " )
return { " src " : " " , " alt " : " image " }
try :
if inline_images :
2026-01-09 17:48:45 +08:00
result = mammoth . convert_to_html ( docx_file , convert_image = mammoth . images . img_element ( _convert_image_to_base64 ) )
2025-10-10 09:39:15 +08:00
else :
result = mammoth . convert_to_html ( docx_file )
html = result . value
markdown_text = markdownify ( html )
return markdown_text
finally :
if not binary :
docx_file . close ( )
2024-08-15 09:17:36 +08:00
class Pdf ( PdfParser ) :
2025-03-17 16:49:54 +08:00
def __init__ ( self ) :
super ( ) . __init__ ( )
2025-03-17 11:58:40 +08:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def __call__ ( self , filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , zoomin = 3 , callback = None , separate_tables_figures = False ) :
2024-08-15 09:17:36 +08:00
start = timer ( )
2024-11-30 18:48:06 +08:00
first_start = start
callback ( msg = " OCR started " )
2026-01-09 17:48:45 +08:00
self . __images__ ( filename if not binary else binary , zoomin , from_page , to_page , callback )
2024-11-30 18:48:06 +08:00
callback ( msg = " OCR finished ( {:.2f} s) " . format ( timer ( ) - start ) )
logging . info ( " OCR( {} ~ {} ): {:.2f} s " . format ( from_page , to_page , timer ( ) - start ) )
2024-08-15 09:17:36 +08:00
start = timer ( )
self . _layouts_rec ( zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.63 , " Layout analysis ( {:.2f} s) " . format ( timer ( ) - start ) )
start = timer ( )
2024-08-15 09:17:36 +08:00
self . _table_transformer_job ( zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.65 , " Table analysis ( {:.2f} s) " . format ( timer ( ) - start ) )
start = timer ( )
2025-10-11 18:46:09 +08:00
self . _text_merge ( zoomin = zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.67 , " Text merged ( {:.2f} s) " . format ( timer ( ) - start ) )
2024-08-15 09:17:36 +08:00
2025-03-20 09:39:32 +08:00
if separate_tables_figures :
tbls , figures = self . _extract_table_figure ( True , zoomin , True , True , True )
self . _concat_downward ( )
logging . info ( " layouts cost: {} s " . format ( timer ( ) - first_start ) )
return [ ( b [ " text " ] , self . _line_tag ( b , zoomin ) ) for b in self . boxes ] , tbls , figures
else :
tbls = self . _extract_table_figure ( True , zoomin , True , True )
2025-08-20 17:29:15 +08:00
self . _naive_vertical_merge ( )
2025-03-20 09:39:32 +08:00
self . _concat_downward ( )
2025-12-30 20:24:27 +08:00
# self._final_reading_order_merge()
2025-03-20 09:39:32 +08:00
# self._filter_forpages()
logging . info ( " layouts cost: {} s " . format ( timer ( ) - first_start ) )
return [ ( b [ " text " ] , self . _line_tag ( b , zoomin ) ) for b in self . boxes ] , tbls
2024-08-15 09:17:36 +08:00
class Markdown ( MarkdownParser ) :
2025-11-03 09:34:12 +08:00
def md_to_html ( self , sections ) :
2025-04-25 18:35:28 +08:00
if not sections :
return [ ]
if isinstance ( sections , type ( " " ) ) :
text = sections
elif isinstance ( sections [ 0 ] , type ( " " ) ) :
text = sections [ 0 ]
else :
return [ ]
2025-07-15 13:03:01 +08:00
2025-04-25 18:35:28 +08:00
from bs4 import BeautifulSoup
2026-01-09 17:48:45 +08:00
2025-05-12 17:16:55 +08:00
html_content = markdown ( text )
2026-01-09 17:48:45 +08:00
soup = BeautifulSoup ( html_content , " html.parser " )
2025-11-03 09:34:12 +08:00
return soup
def get_hyperlink_urls ( self , soup ) :
if soup :
2026-01-09 17:48:45 +08:00
return set ( [ a . get ( " href " ) for a in soup . find_all ( " a " ) if a . get ( " href " ) ] )
2025-11-03 09:34:12 +08:00
return [ ]
2025-11-25 19:54:06 +08:00
def extract_image_urls_with_lines ( self , text ) :
md_img_re = re . compile ( r " ! \ [[^ \ ]]* \ ] \ (([^) \ s]+) " )
html_img_re = re . compile ( r ' src=[ " \\ \' ]([^ " \\ \' > \\ s]+) ' , re . IGNORECASE )
urls = [ ]
seen = set ( )
lines = text . splitlines ( )
for idx , line in enumerate ( lines ) :
for url in md_img_re . findall ( line ) :
if ( url , idx ) not in seen :
urls . append ( { " url " : url , " line " : idx } )
seen . add ( ( url , idx ) )
for url in html_img_re . findall ( line ) :
if ( url , idx ) not in seen :
urls . append ( { " url " : url , " line " : idx } )
seen . add ( ( url , idx ) )
# cross-line
try :
from bs4 import BeautifulSoup
2026-01-09 17:48:45 +08:00
soup = BeautifulSoup ( text , " html.parser " )
2025-11-25 19:54:06 +08:00
newline_offsets = [ m . start ( ) for m in re . finditer ( r " \ n " , text ) ] + [ len ( text ) ]
2026-01-09 17:48:45 +08:00
for img_tag in soup . find_all ( " img " ) :
src = img_tag . get ( " src " )
2025-11-25 19:54:06 +08:00
if not src :
continue
tag_str = str ( img_tag )
pos = text . find ( tag_str )
if pos == - 1 :
# fallback
pos = max ( text . find ( src ) , 0 )
line_no = 0
for i , off in enumerate ( newline_offsets ) :
if pos < = off :
line_no = i
break
if ( src , line_no ) not in seen :
urls . append ( { " url " : src , " line " : line_no } )
seen . add ( ( src , line_no ) )
2025-12-30 15:04:09 +08:00
except Exception as e :
logging . error ( " Failed to extract image urls: {} " . format ( e ) )
2025-11-25 19:54:06 +08:00
pass
return urls
def load_images_from_urls ( self , urls , cache = None ) :
2025-04-25 18:35:28 +08:00
import requests
2025-11-25 19:54:06 +08:00
from pathlib import Path
cache = cache or { }
2025-04-25 18:35:28 +08:00
images = [ ]
2025-11-25 19:54:06 +08:00
for url in urls :
if url in cache :
if cache [ url ] :
images . append ( cache [ url ] )
2025-11-10 13:28:07 +08:00
continue
2025-11-25 19:54:06 +08:00
img_obj = None
2025-04-25 18:35:28 +08:00
try :
2026-01-09 17:48:45 +08:00
if url . startswith ( ( " http:// " , " https:// " ) ) :
2025-07-18 17:06:58 +08:00
response = requests . get ( url , stream = True , timeout = 30 )
2026-01-09 17:48:45 +08:00
if response . status_code == 200 and response . headers . get ( " Content-Type " , " " ) . startswith ( " image/ " ) :
img_obj = Image . open ( BytesIO ( response . content ) ) . convert ( " RGB " )
2025-07-18 17:06:58 +08:00
else :
local_path = Path ( url )
2025-11-25 19:54:06 +08:00
if local_path . exists ( ) :
2026-01-09 17:48:45 +08:00
img_obj = Image . open ( url ) . convert ( " RGB " )
2025-11-25 19:54:06 +08:00
else :
2025-07-18 17:06:58 +08:00
logging . warning ( f " Local image file not found: { url } " )
2025-04-25 18:35:28 +08:00
except Exception as e :
logging . error ( f " Failed to download/open image from { url } : { e } " )
2025-11-25 19:54:06 +08:00
cache [ url ] = img_obj
if img_obj :
images . append ( img_obj )
return images , cache
2025-07-15 13:03:01 +08:00
2025-11-25 19:54:06 +08:00
def __call__ ( self , filename , binary = None , separate_tables = True , delimiter = None , return_section_images = False ) :
2024-08-15 09:17:36 +08:00
if binary :
encoding = find_codec ( binary )
txt = binary . decode ( encoding , errors = " ignore " )
else :
with open ( filename , " r " ) as f :
txt = f . read ( )
2025-08-21 09:36:18 +08:00
2026-01-09 17:48:45 +08:00
remainder , tables = self . extract_tables_and_remainder ( f " { txt } \n " , separate_tables = separate_tables )
2025-11-05 16:10:21 +08:00
# To eliminate duplicate tables in chunking result, uncomment code below and set separate_tables to True in line 410.
# extractor = MarkdownElementExtractor(remainder)
2025-08-21 09:36:18 +08:00
extractor = MarkdownElementExtractor ( txt )
2025-11-25 19:54:06 +08:00
image_refs = self . extract_image_urls_with_lines ( txt )
element_sections = extractor . extract_elements ( delimiter , include_meta = True )
sections = [ ]
section_images = [ ]
image_cache = { }
for element in element_sections :
content = element [ " content " ]
start_line = element [ " start_line " ]
end_line = element [ " end_line " ]
urls_in_section = [ ref [ " url " ] for ref in image_refs if start_line < = ref [ " line " ] < = end_line ]
imgs = [ ]
if urls_in_section :
imgs , image_cache = self . load_images_from_urls ( urls_in_section , image_cache )
combined_image = None
if imgs :
combined_image = reduce ( concat_img , imgs ) if len ( imgs ) > 1 else imgs [ 0 ]
sections . append ( ( content , " " ) )
section_images . append ( combined_image )
2024-08-15 09:17:36 +08:00
tbls = [ ]
for table in tables :
2026-01-09 17:48:45 +08:00
tbls . append ( ( ( None , markdown ( table , extensions = [ " markdown.extensions.tables " ] ) ) , " " ) )
2025-11-25 19:54:06 +08:00
if return_section_images :
return sections , tbls , section_images
2024-08-15 09:17:36 +08:00
return sections , tbls
2025-12-29 12:01:18 +08:00
2025-08-14 12:14:03 +08:00
def load_from_xml_v2 ( baseURI , rels_item_xml ) :
"""
Return | _SerializedRelationships | instance loaded with the
relationships contained in * rels_item_xml * . Returns an empty
collection if * rels_item_xml * is | None | .
"""
srels = _SerializedRelationships ( )
if rels_item_xml is not None :
rels_elm = parse_xml ( rels_item_xml )
for rel_elm in rels_elm . Relationship_lst :
2026-01-14 06:08:46 -05:00
if rel_elm . target_ref in ( " ../NULL " , " NULL " ) or rel_elm . target_ref . startswith ( " # " ) :
2025-08-14 12:14:03 +08:00
continue
srels . _srels . append ( _SerializedRelationship ( baseURI , rel_elm ) )
return srels
2024-08-15 09:17:36 +08:00
2025-12-29 12:01:18 +08:00
Fix: Remove hardcoded page limits causing parsing failures on large PDFs (>300 pages) (#14382)
### What problem does this PR solve?
Fixes #14196
## Problem
When using DeepDOC to parse large PDFs (over 1000 pages), the parser
silently truncated processing at 300 pages due to a hardcoded default
`page_to=299` in `RAGFlowPdfParser.__images__()`. This caused:
- **Errors** on pages beyond the limit
- **Poor image quality** as the parser attempted to compensate with
missing page data
- **Inconsistent chunk splitting** between full PDF imports and partial
imports
Additionally, the codebase scattered magic numbers (`299`, `600`,
`10000`, `100000`, `100000000`, `10000000000`, `10**9`) across 22 files
as sentinel values for "parse all pages", making future maintenance
error-prone.
## Root Cause
```python
# deepdoc/parser/pdf_parser.py (before)
def __images__(self, fnm, zoomin=3, page_from=0, page_to=299, callback=None):
# Only the first 300 pages were rendered; everything beyond was silently dropped
```
While most callers in `rag/app/*.py` correctly passed `to_page=100000`,
the base class `RAGFlowPdfParser.__call__()` and `parse_into_bboxes()`
invoked `__images__` **without** forwarding `page_from`/`page_to`,
falling back to the restrictive default of 299.
## Solution
### 1. Define constants in `common/constants.py`
```python
MAXIMUM_PAGE_NUMBER = 100000 # Used by the parsing layer
MAXIMUM_TASK_PAGE_NUMBER = MAXIMUM_PAGE_NUMBER * 1000 # Used by the task/DB layer
```
### 2. Replace all hardcoded sentinel values
| Layer | Files Changed | Old Values | New Value |
|---|---|---|---|
| **Deepdoc parsers** | `pdf_parser.py`, `mineru_parser.py`,
`docling_parser.py`, `opendataloader_parser.py`, `paddleocr_parser.py`,
`docx_parser.py` | `299`, `600`, `10**9`, `100000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Chunk parsers** | `naive.py`, `book.py`, `qa.py`, `one.py`,
`manual.py`, `paper.py`, `presentation.py`, `laws.py`, `resume.py`,
`email.py`, `table.py` | `100000`, `10000`, `10000000000` |
`MAXIMUM_PAGE_NUMBER` |
| **Task/DB layer** | `db_models.py`, `task_service.py`,
`document_service.py`, `file_service.py` | `100000000` |
`MAXIMUM_TASK_PAGE_NUMBER` |
### 3. Fix `parse_into_bboxes()` missing parameters
Added `from_page`/`to_page` parameters to `parse_into_bboxes()` so that
the `rag/flow/parser/parser.py` DeepDOC path no longer falls back to the
restrictive default.
## Files Changed (22)
- `common/constants.py`
- `deepdoc/parser/pdf_parser.py`
- `deepdoc/parser/mineru_parser.py`
- `deepdoc/parser/docling_parser.py`
- `deepdoc/parser/opendataloader_parser.py`
- `deepdoc/parser/paddleocr_parser.py`
- `deepdoc/parser/docx_parser.py`
- `rag/app/naive.py`
- `rag/app/book.py`
- `rag/app/qa.py`
- `rag/app/one.py`
- `rag/app/manual.py`
- `rag/app/paper.py`
- `rag/app/presentation.py`
- `rag/app/laws.py`
- `rag/app/resume.py`
- `rag/app/email.py`
- `rag/app/table.py`
- `api/db/db_models.py`
- `api/db/services/task_service.py`
- `api/db/services/document_service.py`
- `api/db/services/file_service.py`
### Type of change
- [x] Bug Fix (non-breaking change which fixes an issue)
- [x] Refactoring
---------
Signed-off-by: noob <yixiao121314@outlook.com>
2026-04-27 06:57:20 +00:00
def chunk ( filename , binary = None , from_page = 0 , to_page = MAXIMUM_PAGE_NUMBER , lang = " Chinese " , callback = None , * * kwargs ) :
2024-08-15 09:17:36 +08:00
"""
2026-01-09 17:48:45 +08:00
Supported file formats are docx , pdf , excel , txt .
This method apply the naive ways to chunk files .
Successive text will be sliced into pieces using ' delimiter ' .
Next , these successive pieces are merge into chunks whose token number is no more than ' Max token number ' .
2024-08-15 09:17:36 +08:00
"""
2025-11-03 09:34:12 +08:00
urls = set ( )
url_res = [ ]
2025-10-27 17:04:13 +08:00
2026-05-22 11:46:38 +08:00
lang = lang or " Chinese "
2024-12-01 22:28:00 +08:00
is_english = lang . lower ( ) == " english " # is_english(cks)
2026-01-09 17:48:45 +08:00
parser_config = kwargs . get ( " parser_config " , { " chunk_token_num " : 512 , " delimiter " : " \n !?。;!? " , " layout_recognize " : " DeepDOC " , " analyze_hyperlink " : True } )
2025-11-28 19:25:32 +08:00
2026-01-09 17:48:45 +08:00
child_deli = ( parser_config . get ( " children_delimiter " ) or " " ) . encode ( " utf-8 " ) . decode ( " unicode_escape " ) . encode ( " latin1 " ) . decode ( " utf-8 " )
2025-12-17 16:50:36 +08:00
cust_child_deli = re . findall ( r " `([^`]+)` " , child_deli )
child_deli = " | " . join ( re . sub ( r " `([^`]+)` " , " " , child_deli ) )
if cust_child_deli :
cust_child_deli = sorted ( set ( cust_child_deli ) , key = lambda x : - len ( x ) )
cust_child_deli = " | " . join ( re . escape ( t ) for t in cust_child_deli if t )
child_deli + = cust_child_deli
2025-11-28 19:25:32 +08:00
is_markdown = False
2025-11-27 10:21:44 +08:00
table_context_size = max ( 0 , int ( parser_config . get ( " table_context_size " , 0 ) or 0 ) )
image_context_size = max ( 0 , int ( parser_config . get ( " image_context_size " , 0 ) or 0 ) )
2025-11-28 19:25:32 +08:00
2026-01-09 17:48:45 +08:00
doc = { " docnm_kwd " : filename , " title_tks " : rag_tokenizer . tokenize ( re . sub ( r " \ .[a-zA-Z]+$ " , " " , filename ) ) }
2024-08-15 09:17:36 +08:00
doc [ " title_sm_tks " ] = rag_tokenizer . fine_grained_tokenize ( doc [ " title_tks " ] )
res = [ ]
pdf_parser = None
2025-04-25 18:35:28 +08:00
section_images = None
2025-10-17 18:46:47 +08:00
is_root = kwargs . get ( " is_root " , True )
embed_res = [ ]
if is_root :
# Only extract embedded files at the root call
embeds = [ ]
if binary is not None :
embeds = extract_embed_file ( binary )
else :
raise Exception ( " Embedding extraction from file path is not supported. " )
2025-10-27 17:04:13 +08:00
2025-10-17 18:46:47 +08:00
# Recursively chunk each embedded file and collect results
for embed_filename , embed_bytes in embeds :
try :
2026-01-09 17:48:45 +08:00
sub_res = chunk ( embed_filename , binary = embed_bytes , lang = lang , callback = callback , is_root = False , * * kwargs ) or [ ]
2025-10-17 18:46:47 +08:00
embed_res . extend ( sub_res )
except Exception as e :
2025-12-30 15:04:09 +08:00
error_msg = f " Failed to chunk embed { embed_filename } : { e } "
logging . error ( error_msg )
2025-10-17 18:46:47 +08:00
if callback :
2025-12-30 15:04:09 +08:00
callback ( 0.05 , error_msg )
2025-10-17 18:46:47 +08:00
continue
2024-08-15 09:17:36 +08:00
if re . search ( r " \ .docx$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-11-03 09:34:12 +08:00
if parser_config . get ( " analyze_hyperlink " , False ) and is_root :
urls = extract_links_from_docx ( binary )
for index , url in enumerate ( urls ) :
html_bytes , metadata = extract_html ( url )
if not html_bytes :
continue
try :
sub_url_res = chunk ( url , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
except Exception as e :
logging . info ( f " Failed to chunk url in registered file type { url } : { e } " )
2026-01-09 17:48:45 +08:00
sub_url_res = chunk ( f " { index } .html " , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
2025-11-03 09:34:12 +08:00
url_res . extend ( sub_url_res )
2025-03-20 11:24:44 +08:00
2025-08-14 12:14:03 +08:00
# fix "There is no item named 'word/NULL' in the archive", referring to https://github.com/python-openxml/python-docx/issues/1105#issuecomment-1298075246
_SerializedRelationships . load_from_xml = load_from_xml_v2
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
# sections = (text, image, tables)
sections = Docx ( ) ( filename , binary )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
# chunks list[dict]
# images list - index of image chunk in chunks
2026-01-09 17:48:45 +08:00
chunks , images = naive_merge_docx ( sections , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , table_context_size , image_context_size )
2026-01-07 15:08:17 +08:00
vision_figure_parser_docx_wrapper_naive ( chunks = chunks , idx_lst = images , callback = callback , * * kwargs )
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
callback ( 0.8 , " Finish parsing. " )
st = timer ( )
res . extend ( doc_tokenize_chunks_with_images ( chunks , doc , is_english , child_delimiters_pattern = child_deli ) )
2024-11-14 17:13:48 +08:00
logging . info ( " naive_merge( {} ): {} " . format ( filename , timer ( ) - st ) )
2025-10-17 18:46:47 +08:00
res . extend ( embed_res )
2025-11-03 09:34:12 +08:00
res . extend ( url_res )
2024-08-15 09:17:36 +08:00
return res
elif re . search ( r " \ .pdf$ " , filename , re . IGNORECASE ) :
2026-01-09 17:48:45 +08:00
layout_recognizer , parser_model_name = normalize_layout_recognizer ( parser_config . get ( " layout_recognize " , " DeepDOC " ) )
2026-05-06 15:00:55 +08:00
opendataloader_llm_name = kwargs . pop ( " opendataloader_llm_name " , None )
if layout_recognizer == " OpenDataLoader " and parser_model_name :
opendataloader_llm_name = parser_model_name
2025-12-09 18:54:14 +08:00
2025-11-03 09:34:12 +08:00
if parser_config . get ( " analyze_hyperlink " , False ) and is_root :
urls = extract_links_from_pdf ( binary )
2025-03-20 14:50:59 +08:00
if isinstance ( layout_recognizer , bool ) :
2026-03-04 11:51:10 +08:00
layout_recognizer = " DeepDOC " if layout_recognizer else " PlainText "
2025-03-20 09:39:32 +08:00
2025-11-05 13:00:42 +08:00
name = layout_recognizer . strip ( ) . lower ( )
2025-11-06 15:20:35 +08:00
parser = PARSERS . get ( name , by_plaintext )
2025-11-05 13:00:42 +08:00
callback ( 0.1 , " Start to parse. " )
2025-10-17 18:46:47 +08:00
2025-11-06 15:20:35 +08:00
sections , tables , pdf_parser = parser (
2025-12-29 12:01:18 +08:00
filename = filename ,
binary = binary ,
from_page = from_page ,
to_page = to_page ,
lang = lang ,
callback = callback ,
layout_recognizer = layout_recognizer ,
mineru_llm_name = parser_model_name ,
2026-01-09 17:48:45 +08:00
paddleocr_llm_name = parser_model_name ,
2026-05-06 15:00:55 +08:00
opendataloader_llm_name = opendataloader_llm_name ,
2026-01-09 17:48:45 +08:00
* * kwargs ,
2025-11-05 13:00:42 +08:00
)
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-10-23 19:44:25 +08:00
2025-11-05 13:00:42 +08:00
if not sections and not tables :
return [ ]
2025-10-27 15:14:58 +08:00
2025-12-30 20:24:27 +08:00
if table_context_size or image_context_size :
tables = append_context2table_image4pdf ( sections , tables , image_context_size )
Feat: add OpenDataLoader PDF parser backend (#14058) (#14097)
### What problem does this PR solve?
Closes #14058.
RAGFlow supports multiple PDF parsing backends (DeepDOC, MinerU,
Docling, TCADP, PaddleOCR). This PR adds **OpenDataLoader**
([opendataloader-project/opendataloader-pdf](https://github.com/opendataloader-project/opendataloader-pdf))
as a new optional backend, giving users a deterministic, local-first
alternative with competitive table extraction accuracy.
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- [x] Documentation Update
---
### Changes
#### Backend
- `deepdoc/parser/opendataloader_parser.py` — new `OpenDataLoaderParser`
class inheriting `RAGFlowPdfParser`. Implements `check_installation()`
(guards Python package + Java 11+ runtime), `parse_pdf()` with
JSON-first extraction (heading/paragraph/table/list/image/formula) and
Markdown fallback, position-tag generation compatible with the shared
`@@page\tx0\tx1\ty0\ty1##` format, and temp-dir lifecycle with cleanup.
- `rag/app/naive.py` — new `by_opendataloader()` wrapper, registered in
`PARSERS` dict, added to `chunk_token_num=0` override list.
- `rag/flow/parser/parser.py` — `"opendataloader"` branch in the
pipeline PDF handler + check validation list.
#### Infrastructure
- `docker/entrypoint.sh` — `ensure_opendataloader()` function: opt-in
via `USE_OPENDATALOADER=true`, skips gracefully if Java is not on PATH.
#### Frontend
- `web/src/components/layout-recognize-form-field.tsx` —
`OpenDataLoader` added to `ParseDocumentType` enum and parser dropdown.
Cascades automatically to the pipeline editor's Parser component.
#### Docs
- `docs/guides/dataset/select_pdf_parser.md` — added OpenDataLoader
entry and full env-var reference.
---
### Environment variables
| Variable | Default | Description |
|---|---|---|
| `USE_OPENDATALOADER` | `false` | Set `true` to install
`opendataloader-pdf` on container startup |
| `OPENDATALOADER_VERSION` | latest | Pin the PyPI release (e.g.
`==2.2.1`) |
| `OPENDATALOADER_HYBRID` | _(unset)_ | Enable hybrid AI mode (e.g.
`docling-fast`) |
| `OPENDATALOADER_IMAGE_OUTPUT` | _(unset)_ | `off` / `embedded` /
`external` |
| `OPENDATALOADER_OUTPUT_DIR` | _(tmp)_ | Persistent output dir; temp
dir used + cleaned if unset |
| `OPENDATALOADER_DELETE_OUTPUT` | `1` | `0` to retain intermediate
files for debugging |
| `OPENDATALOADER_SANITIZE` | _(unset)_ | `1` to filter prompt-injection
patterns from output |
---
### Dependencies
- **Runtime**: `opendataloader-pdf` (PyPI, Apache 2.0) — opt-in, not
added to `pyproject.toml` core deps. Installed by
`ensure_opendataloader()` at container startup when
`USE_OPENDATALOADER=true`.
- **System**: Java 11+ on PATH (JVM is the underlying engine). The
installer skips with a warning if `java` is not found.
---
### How to test
**Standalone parser:**
```bash
source .venv/bin/activate
uv pip install opendataloader-pdf
python3 -c "
import sys; sys.path.insert(0, '.')
from deepdoc.parser.opendataloader_parser import OpenDataLoaderParser
p = OpenDataLoaderParser()
print('available:', p.check_installation())
s, t = p.parse_pdf('path/to/test.pdf', parse_method='pipeline')
print(f'sections={len(s)} tables={len(t)}')
"
```
### Benchmark vs Docling
```
file parser secs sections tables
----------------------------------------------------------------------
text-heavy.pdf docling 45.29 148 10
text-heavy.pdf opendataloader 3.14 559 0
table-heavy.pdf docling 7.05 76 3
table-heavy.pdf opendataloader 3.71 90 0
complex.pdf docling 42.67 114 8
complex.pdf opendataloader 3.51 180 0
```
2026-04-24 18:33:02 +02:00
if name in [ " tcadp " , " docling " , " mineru " , " paddleocr " , " opendataloader " ] :
Fix: respect user-configured chunk_token_num for MinerU/docling/paddleocr parsers (#13234)
## Summary
When using MinerU, docling, TCADP, or paddleocr as the PDF parser with
the General (naive) chunk method, the user-configured `chunk_token_num`
is **unconditionally overwritten to 0** at
[rag/app/naive.py#L858-L859](https://github.com/infiniflow/ragflow/blob/main/rag/app/naive.py#L858-L859),
effectively disabling chunk merging regardless of what the user sets in
the UI.
### Problem
A user sets `chunk_token_num = 2048` in the dataset configuration UI,
expecting small parser blocks to be merged into larger chunks. However,
this line:
```python
if name in ["tcadp", "docling", "mineru", "paddleocr"]:
parser_config["chunk_token_num"] = 0
```
silently overrides the user's setting. As a result, every MinerU output
block becomes its own chunk. For short documents (e.g. a 3-page PDF fund
factsheet parsed by MinerU), this produces **47 tiny chunks** — some as
small as 11 characters (`"July 2025"`) or 15 characters (`"CIES
Eligible"`).
This severely degrades retrieval quality: vector embeddings of such
short fragments have minimal semantic value, and keyword search produces
excessive noise.
### Fix
Only apply the `chunk_token_num = 0` override when the user has **not**
explicitly configured a positive value:
```python
if name in ["tcadp", "docling", "mineru", "paddleocr"]:
if int(parser_config.get("chunk_token_num", 0)) <= 0:
parser_config["chunk_token_num"] = 0
```
This preserves the original default behavior (no merging) while
respecting the user's explicit configuration.
### Before / After (MinerU, 3-page PDF, chunk_token_num=2048)
| | Before | After |
|---|---|---|
| Chunks produced | 47 | ~8 (merged by token limit) |
| Smallest chunk | 11 chars | ~500 chars |
| User setting respected | No | Yes |
## Test plan
- [ ] Parse a PDF with MinerU and `chunk_token_num = 2048` → verify
chunks are merged up to token limit
- [ ] Parse a PDF with MinerU and `chunk_token_num = 0` (or default) →
verify original behavior (no merging)
- [ ] Parse a PDF with DeepDOC parser → verify no change in behavior
(not affected by this code path)
- [ ] Repeat with docling/paddleocr if available
2026-03-02 15:31:40 +08:00
if int ( parser_config . get ( " chunk_token_num " , 0 ) ) < = 0 :
parser_config [ " chunk_token_num " ] = 0
2025-11-20 10:08:42 +08:00
2025-11-05 13:00:42 +08:00
res = tokenize_table ( tables , doc , is_english )
callback ( 0.8 , " Finish parsing. " )
2024-08-15 09:17:36 +08:00
2025-03-12 19:20:50 +08:00
elif re . search ( r " \ .(csv|xlsx?)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2025-11-20 10:08:42 +08:00
# Check if tcadp_parser is selected for spreadsheet files
layout_recognizer = parser_config . get ( " layout_recognize " , " DeepDOC " )
if layout_recognizer == " TCADP Parser " :
table_result_type = parser_config . get ( " table_result_type " , " 1 " )
markdown_image_response_type = parser_config . get ( " markdown_image_response_type " , " 1 " )
2026-01-09 17:48:45 +08:00
tcadp_parser = TCADPParser ( table_result_type = table_result_type , markdown_image_response_type = markdown_image_response_type )
2025-11-20 10:08:42 +08:00
if not tcadp_parser . check_installation ( ) :
callback ( - 1 , " TCADP parser not available. Please check Tencent Cloud API configuration. " )
return res
# Determine file type based on extension
file_type = " XLSX " if re . search ( r " \ .xlsx?$ " , filename , re . IGNORECASE ) else " CSV "
2026-01-09 17:48:45 +08:00
sections , tables = tcadp_parser . parse_pdf ( filepath = filename , binary = binary , callback = callback , output_dir = os . environ . get ( " TCADP_OUTPUT_DIR " , " " ) , file_type = file_type )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-11-20 10:08:42 +08:00
parser_config [ " chunk_token_num " ] = 0
res = tokenize_table ( tables , doc , is_english )
callback ( 0.8 , " Finish parsing. " )
2024-09-20 15:33:38 +08:00
else :
2025-11-20 10:08:42 +08:00
# Default DeepDOC parser
excel_parser = ExcelParser ( )
if parser_config . get ( " html4excel " ) :
sections = [ ( _ , " " ) for _ in excel_parser . html ( binary , 12 ) if _ ]
2025-11-25 19:54:20 +08:00
parser_config [ " chunk_token_num " ] = 0
2025-11-20 10:08:42 +08:00
else :
sections = [ ( _ , " " ) for _ in excel_parser ( binary ) if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
elif re . search ( r " \ .(txt|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|sql)$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2026-01-09 17:48:45 +08:00
sections = TxtParser ( ) ( filename , binary , parser_config . get ( " chunk_token_num " , 128 ) , parser_config . get ( " delimiter " , " \n !?;。;!? " ) )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2026-04-10 10:03:44 +08:00
print ( " \n " , " - " * 150 , " \n " )
print ( sections )
print ( " \n " , " - " * 150 , " \n " )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
2024-10-09 19:37:32 +08:00
2025-12-29 12:54:31 +08:00
elif re . search ( r " \ .(md|markdown|mdx)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2025-04-25 18:35:28 +08:00
markdown_parser = Markdown ( int ( parser_config . get ( " chunk_token_num " , 128 ) ) )
2025-11-25 19:54:06 +08:00
sections , tables , section_images = markdown_parser (
filename ,
binary ,
separate_tables = False ,
delimiter = parser_config . get ( " delimiter " , " \n !?;。;!? " ) ,
return_section_images = True ,
)
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-11-25 19:54:06 +08:00
2025-11-28 19:25:32 +08:00
is_markdown = True
2025-07-15 13:03:01 +08:00
2025-09-18 09:44:17 +08:00
try :
2026-03-05 17:27:17 +08:00
vision_model_config = get_tenant_default_model_by_type ( kwargs [ " tenant_id " ] , LLMType . IMAGE2TEXT )
vision_model = LLMBundle ( kwargs [ " tenant_id " ] , vision_model_config )
2025-09-18 09:44:17 +08:00
callback ( 0.2 , " Visual model detected. Attempting to enhance figure extraction... " )
2025-12-30 15:04:09 +08:00
except Exception as e :
logging . warning ( f " Failed to detect figure extraction: { e } " )
2025-09-18 09:44:17 +08:00
vision_model = None
2025-10-10 09:39:15 +08:00
2025-09-18 09:44:17 +08:00
if vision_model :
# Process images for each section
for idx , ( section_text , _ ) in enumerate ( sections ) :
2025-11-25 19:54:06 +08:00
images = [ ]
if section_images and len ( section_images ) > idx and section_images [ idx ] is not None :
images . append ( section_images [ idx ] )
2025-09-18 09:44:17 +08:00
2025-11-25 19:54:06 +08:00
if images and len ( images ) > 0 :
2025-09-18 09:44:17 +08:00
# If multiple images found, combine them using concat_img
combined_image = reduce ( concat_img , images ) if len ( images ) > 1 else images [ 0 ]
2025-11-25 19:54:06 +08:00
if section_images :
section_images [ idx ] = combined_image
else :
section_images = [ None ] * len ( sections )
section_images [ idx ] = combined_image
2026-01-09 17:48:45 +08:00
markdown_vision_parser = VisionFigureParser ( vision_model = vision_model , figures_data = [ ( ( combined_image , [ " markdown image " ] ) , [ ( 0 , 0 , 0 , 0 , 0 ) ] ) ] , * * kwargs )
2025-09-18 09:44:17 +08:00
boosted_figures = markdown_vision_parser ( callback = callback )
2026-01-09 17:48:45 +08:00
sections [ idx ] = ( section_text + " \n \n " + " \n \n " . join ( [ fig [ 0 ] [ 1 ] for fig in boosted_figures ] ) , sections [ idx ] [ 1 ] )
2025-11-03 09:34:12 +08:00
2025-09-18 09:44:17 +08:00
else :
logging . warning ( " No visual model detected. Skipping figure parsing enhancement. " )
2025-07-15 13:03:01 +08:00
2025-11-03 09:34:12 +08:00
if parser_config . get ( " hyperlink_urls " , False ) and is_root :
for idx , ( section_text , _ ) in enumerate ( sections ) :
soup = markdown_parser . md_to_html ( section_text )
hyperlink_urls = markdown_parser . get_hyperlink_urls ( soup )
urls . update ( hyperlink_urls )
2024-12-01 22:28:00 +08:00
res = tokenize_table ( tables , doc , is_english )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
elif re . search ( r " \ .(htm|html)$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-08-27 12:43:55 +08:00
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = HtmlParser ( ) ( filename , binary , chunk_token_num )
2024-09-29 10:29:56 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
2026-03-17 15:14:06 +03:00
elif re . search ( r " \ .epub$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = EpubParser ( ) ( filename , binary , chunk_token_num )
sections = [ ( _ , " " ) for _ in sections if _ ]
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
callback ( 0.8 , " Finish parsing. " )
2025-07-30 09:48:20 +08:00
elif re . search ( r " \ .(json|jsonl|ldjson)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2024-12-03 19:02:03 +08:00
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = JsonParser ( chunk_token_num ) ( binary )
2024-09-29 10:29:56 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
elif re . search ( r " \ .doc$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-11-03 09:34:12 +08:00
2025-11-07 11:46:10 +08:00
try :
from tika import parser as tika_parser
except Exception as e :
callback ( 0.8 , f " tika not available: { e } . Unsupported .doc parsing. " )
logging . warning ( f " tika not available: { e } . Unsupported .doc parsing for { filename } . " )
return [ ]
2024-08-15 09:17:36 +08:00
binary = BytesIO ( binary )
2025-11-07 11:46:10 +08:00
doc_parsed = tika_parser . from_buffer ( binary )
2026-01-09 17:48:45 +08:00
if doc_parsed . get ( " content " , None ) is not None :
sections = doc_parsed [ " content " ] . split ( " \n " )
2024-11-22 11:05:06 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-11-22 11:05:06 +08:00
callback ( 0.8 , " Finish parsing. " )
else :
2025-12-30 15:04:09 +08:00
error_msg = f " tika.parser got empty content from { filename } . "
callback ( 0.8 , error_msg )
logging . warning ( error_msg )
2024-11-22 11:05:06 +08:00
return [ ]
2024-08-15 09:17:36 +08:00
else :
2026-01-09 17:48:45 +08:00
raise NotImplementedError ( " file type not supported yet(pdf, xlsx, doc, docx, txt supported) " )
2024-08-15 09:17:36 +08:00
st = timer ( )
2026-01-27 12:43:01 +08:00
overlapped_percent = normalize_overlapped_percent ( parser_config . get ( " overlapped_percent " , 0 ) )
2026-06-03 10:49:28 +08:00
2025-11-28 19:25:32 +08:00
if is_markdown :
2025-11-25 19:54:06 +08:00
merged_chunks = [ ]
merged_images = [ ]
chunk_limit = max ( 0 , int ( parser_config . get ( " chunk_token_num " , 128 ) ) )
current_text = " "
current_tokens = 0
current_image = None
for idx , sec in enumerate ( sections ) :
text = sec [ 0 ] if isinstance ( sec , tuple ) else sec
sec_tokens = num_tokens_from_string ( text )
sec_image = section_images [ idx ] if section_images and idx < len ( section_images ) else None
2026-06-03 10:49:28 +08:00
# Don't finalize chunk if current_text is a short header (force merge with next section)
if current_text and not _is_short_header ( current_text ) and current_tokens + sec_tokens > chunk_limit :
2025-11-25 19:54:06 +08:00
merged_chunks . append ( current_text )
merged_images . append ( current_image )
overlap_part = " "
if overlapped_percent > 0 :
overlap_len = int ( len ( current_text ) * overlapped_percent / 100 )
if overlap_len > 0 :
overlap_part = current_text [ - overlap_len : ]
current_text = overlap_part
current_tokens = num_tokens_from_string ( current_text )
current_image = current_image if overlap_part else None
if current_text :
current_text + = " \n " + text
else :
current_text = text
current_tokens + = sec_tokens
2025-07-15 13:03:01 +08:00
2025-11-25 19:54:06 +08:00
if sec_image :
current_image = concat_img ( current_image , sec_image ) if current_image else sec_image
if current_text :
merged_chunks . append ( current_text )
merged_images . append ( current_image )
chunks = merged_chunks
has_images = merged_images and any ( img is not None for img in merged_images )
2025-11-28 19:25:32 +08:00
2025-11-25 19:54:06 +08:00
if has_images :
2026-01-09 17:48:45 +08:00
res . extend ( tokenize_chunks_with_images ( chunks , doc , is_english , merged_images , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
2025-11-28 19:25:32 +08:00
res . extend ( tokenize_chunks ( chunks , doc , is_english , pdf_parser , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
if section_images :
if all ( image is None for image in section_images ) :
section_images = None
if section_images :
2026-01-27 12:43:01 +08:00
chunks , images = naive_merge_with_images ( sections , section_images , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , overlapped_percent )
2026-01-09 17:48:45 +08:00
res . extend ( tokenize_chunks_with_images ( chunks , doc , is_english , images , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
2026-01-27 12:43:01 +08:00
chunks = naive_merge ( sections , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , overlapped_percent )
2025-04-25 18:35:28 +08:00
2025-11-28 19:25:32 +08:00
res . extend ( tokenize_chunks ( chunks , doc , is_english , pdf_parser , child_delimiters_pattern = child_deli ) )
2025-07-15 13:03:01 +08:00
2025-11-03 09:34:12 +08:00
if urls and parser_config . get ( " analyze_hyperlink " , False ) and is_root :
for index , url in enumerate ( urls ) :
html_bytes , metadata = extract_html ( url )
if not html_bytes :
continue
try :
sub_url_res = chunk ( url , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
except Exception as e :
logging . info ( f " Failed to chunk url in registered file type { url } : { e } " )
sub_url_res = chunk ( f " { index } .html " , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
url_res . extend ( sub_url_res )
2025-11-25 19:54:06 +08:00
2024-11-14 17:13:48 +08:00
logging . info ( " naive_merge( {} ): {} " . format ( filename , timer ( ) - st ) )
2025-11-25 19:54:06 +08:00
2025-10-17 18:46:47 +08:00
if embed_res :
res . extend ( embed_res )
2025-11-03 09:34:12 +08:00
if url_res :
res . extend ( url_res )
2026-01-07 15:08:17 +08:00
# if table_context_size or image_context_size:
2025-12-30 20:24:27 +08:00
# attach_media_context(res, table_context_size, image_context_size)
feat: persist PDF bookmark outline as document metadata (#13287)
## Summary
PDF files often contain a bookmark/outline tree (table of contents built
into the file by the authoring tool). RAGFlow's `pdf_parser.outlines`
already extracts these `(title, depth)` tuples via pypdf, but they are
used ephemerally during chunking (`manual` parser uses them for
hierarchy detection) and then discarded.
This PR persists the outline as `doc.meta_fields["outline"]` — a JSON
array of `{"title": str, "depth": int}` objects — so downstream features
can use the structural information.
### Why this matters
- **Complementary to `toc_extraction`** — the existing `toc_extraction`
feature uses LLM calls to generate a TOC and only works for the `naive`
parser. The raw PDF outline is free (already extracted by pypdf), works
for all parsers, and captures the author's original document structure.
- **Document navigation** — frontends can render a clickable TOC from
the outline
- **Entity extraction** — the outline provides a structural map for
identifying document sections and key topics
- **Search result context** — knowing which section a chunk belongs to
helps users evaluate relevance
### Changes
| File | Change | LOC |
|------|--------|-----|
| `rag/app/naive.py` | Attach `pdf_parser.outlines` as `__outline__` on
first chunk dict | ~7 |
| `rag/app/manual.py` | Same for the manual parser | ~5 |
| `rag/svr/task_executor.py` | Extract `__outline__`, persist via
`DocMetadataService.update_document_metadata()` | ~12 |
### Design decisions
- **Transient key pattern**: The outline is passed from parser →
task_executor via `__outline__` on the first chunk dict, then removed
before indexing. This follows the same pattern as `metadata_obj` for
LLM-generated metadata.
- **No schema changes**: Uses the existing `meta_fields` JSON column on
the document table.
- **Graceful degradation**: If a PDF has no outline (common for scanned
docs), nothing is stored. If persistence fails, it logs a warning and
continues — parsing is not interrupted.
### Backward compatibility
- **Fully backward compatible** — no existing fields, behavior, or
schemas changed
- PDFs without outlines are unaffected
- Existing `meta_fields` data is preserved (merged, not overwritten)
## Test plan
- [ ] Parse a PDF with bookmarks (e.g. any multi-chapter document),
verify `meta_fields["outline"]` is populated
- [ ] Parse a PDF without bookmarks, verify no errors and no outline key
in meta_fields
- [ ] Verify existing `meta_fields` data is preserved (not overwritten)
when outline is added
- [ ] Verify `manual` parser also persists outlines
- [ ] Verify outline JSON structure: `[{"title": "Chapter 1", "depth":
0}, ...]`
Related: #9921 (Deterministic Document Access Layer)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: yuch85 <yuch85.1@gmail.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-04-27 11:57:06 +08:00
# Attach PDF outline as transient metadata on the first chunk.
# task_executor.py will extract and persist it as document metadata.
if res and pdf_parser and getattr ( pdf_parser , " outlines " , None ) :
res [ 0 ] [ " __outline__ " ] = [
{ " title " : title , " depth " : depth }
2026-04-30 11:55:02 +08:00
for title , depth , * _ in pdf_parser . outlines
feat: persist PDF bookmark outline as document metadata (#13287)
## Summary
PDF files often contain a bookmark/outline tree (table of contents built
into the file by the authoring tool). RAGFlow's `pdf_parser.outlines`
already extracts these `(title, depth)` tuples via pypdf, but they are
used ephemerally during chunking (`manual` parser uses them for
hierarchy detection) and then discarded.
This PR persists the outline as `doc.meta_fields["outline"]` — a JSON
array of `{"title": str, "depth": int}` objects — so downstream features
can use the structural information.
### Why this matters
- **Complementary to `toc_extraction`** — the existing `toc_extraction`
feature uses LLM calls to generate a TOC and only works for the `naive`
parser. The raw PDF outline is free (already extracted by pypdf), works
for all parsers, and captures the author's original document structure.
- **Document navigation** — frontends can render a clickable TOC from
the outline
- **Entity extraction** — the outline provides a structural map for
identifying document sections and key topics
- **Search result context** — knowing which section a chunk belongs to
helps users evaluate relevance
### Changes
| File | Change | LOC |
|------|--------|-----|
| `rag/app/naive.py` | Attach `pdf_parser.outlines` as `__outline__` on
first chunk dict | ~7 |
| `rag/app/manual.py` | Same for the manual parser | ~5 |
| `rag/svr/task_executor.py` | Extract `__outline__`, persist via
`DocMetadataService.update_document_metadata()` | ~12 |
### Design decisions
- **Transient key pattern**: The outline is passed from parser →
task_executor via `__outline__` on the first chunk dict, then removed
before indexing. This follows the same pattern as `metadata_obj` for
LLM-generated metadata.
- **No schema changes**: Uses the existing `meta_fields` JSON column on
the document table.
- **Graceful degradation**: If a PDF has no outline (common for scanned
docs), nothing is stored. If persistence fails, it logs a warning and
continues — parsing is not interrupted.
### Backward compatibility
- **Fully backward compatible** — no existing fields, behavior, or
schemas changed
- PDFs without outlines are unaffected
- Existing `meta_fields` data is preserved (merged, not overwritten)
## Test plan
- [ ] Parse a PDF with bookmarks (e.g. any multi-chapter document),
verify `meta_fields["outline"]` is populated
- [ ] Parse a PDF without bookmarks, verify no errors and no outline key
in meta_fields
- [ ] Verify existing `meta_fields` data is preserved (not overwritten)
when outline is added
- [ ] Verify `manual` parser also persists outlines
- [ ] Verify outline JSON structure: `[{"title": "Chapter 1", "depth":
0}, ...]`
Related: #9921 (Deterministic Document Access Layer)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: yuch85 <yuch85.1@gmail.com>
Co-authored-by: Wang Qi <wangq8@outlook.com>
2026-04-27 11:57:06 +08:00
]
2024-08-15 09:17:36 +08:00
return res
if __name__ == " __main__ " :
import sys
def dummy ( prog = None , msg = " " ) :
pass
chunk ( sys . argv [ 1 ] , from_page = 0 , to_page = 10 , callback = dummy )