PDF Chunk Position Rendering#
RAGFlow highlights the source location of each retrieved PDF chunk in its document viewer. This requires converting bounding-box coordinates from the parser's output space into the coordinate system expected by the React frontend. The pipeline has three stages: (1) parser → canonical storage format, (2) API response returning positions arrays to the frontend, and (3) frontend rendering using react-pdf-highlighter.
Canonical Position Format#
Every PDF chunk stores positions as a list of 5-element tuples:
[page_number, left, right, top, bottom] # integers, 1-based page numbers
This is the position_int field persisted in the chunk store . The helper build_pdf_position_fields() converts floating-point normalized positions into integer position_int, page_num_int, and top_int fields used for sorting and retrieval.
extract_pdf_positions() is the central normalizer. It accepts positions from any of four formats and emits a canonical list:
_pdf_positions(already-normalized internal field)positions(direct list)position_tagstring — parsed byRAGFlowPdfParser.extract_positions()using the embedded format@@{pages}\t{x0}\t{x1}\t{top}\t{bottom}##- Individual fields:
page_number,x0,x1,top,bottom
Page numbers of 0 or -N (0-based from some parsers) are corrected to 1-based during normalization .
Parser-Specific Coordinate Spaces#
Different parsers produce coordinates in different spaces; each must be normalized before storage.
DeepDoc (RAGFlowPdfParser)#
Uses pixel space with a top-left origin, at 72 × zoom DPI (where zoom defaults to 3, so 216 DPI for rendering). Position tags are encoded as @@{page}\t{x0}\t{x1}\t{top}\t{bottom}## . The Go parser mirrors this, with an explicit DlaScale = 3.0 factor used to scale OCR-detected boxes from 216 DPI down to 72 DPI PDF space .
MinerU Parser#
MinerU outputs bounding boxes in 0–1000 normalized coordinates relative to each page's width and height. _line_tag() in mineru_parser.py converts these to pixel coordinates by multiplying (bbox_value / 1000.0) × page_dimension . When rendered page images are unavailable, the parser falls back to captured page sizes, then to a default page size, ensuring coordinates are always in display space rather than raw normalized values .
MinerU also returns inverted (x0 > x1, top > bottom) boxes for some images; these are swapped before use .
Cross-Page Table Problem (MinerU)#
MinerU merges a cross-page table into a single content_list.json item but retains only the first fragment's bbox/page_idx. Before the fix in PR #16283, RAGFlow faithfully rendered that single bbox, displacing highlights to the page-header area for tall multi-page tables . The fix reads middle.json (which still has per-page fragments), normalizes those fragments into 0–1000 space, and emits one highlight tag per page the table spans .
Large-PDF Chunked Parsing#
When a PDF exceeds PDF_PARSER_PAGE_BATCH_SIZE (default: 50 pages), DeepDoc parses in batches. _to_global_boxes() adds the batch's page_from offset to every page_number value and re-offsets position_tag strings via _offset_position_tag(), ensuring chunk-local page indices are promoted to document-global ones .
Thumbnail Preview Generation (Backend)#
_crop_pdf_preview() produces the thumbnail shown alongside each chunk reference:
- Pages are rasterized at
72 × PDF_PREVIEW_ZOOMDPI (PDF_PREVIEW_ZOOM = 3) . - Coordinates are scaled by
zoomwhen cropping pixel data:left * zoom,top * zoom, etc. . - Context strips (
PDF_PREVIEW_CONTEXT = 120points above and below the chunk,PDF_PREVIEW_GAP = 6) are added and dimmed with a 50 % alpha overlay so the highlighted region is visually distinct . - For multi-position chunks, crops from each span are assembled vertically into a single canvas .
restore_pdf_text_previews() orchestrates thumbnail generation inside the agent/flow pipeline, caching previews by position key to avoid re-rendering duplicate spans .
Frontend Rendering#
Highlight Data Shape#
The API returns chunks with a positions field (aliased from position_int). buildChunkHighlights() in document-util.ts converts these into IHighlight objects for react-pdf-highlighter:
x1 = positions[i][1],x2 = positions[i][2],y1 = positions[i][3],y2 = positions[i][4]pageNumber = positions[i][0]widthandheightare populated from the PDF viewport
The viewport dimensions are obtained once from pdfDocument.getPage(1).getViewport({ scale: 1 }) and passed down via setWidthAndHeight. This means width/height always reflect page 1's dimensions; for PDFs with non-uniform page sizes, coordinates on other pages will be passed through with page-1 dimensions — a known limitation.
PdfPreview Component#
PdfPreview wraps react-pdf-highlighter's PdfHighlighter. Highlights are scrolled into view automatically: a useEffect fires scrollTo(highlights[0]) 100 ms after highlights change . The PdfLoader passes authorization headers so the PDF binary can be fetched from the backend storage API .
Key Known Issues & Fixes#
| Issue | Root Cause | Fix |
|---|---|---|
| Highlight displaced to page header on wide MinerU tables | MinerU stores only first page's bbox for merged cross-page tables | PR #16283: reconstruct per-page positions from middle.json |
| "Coordinate lower is less than upper" error with MinerU | MinerU emits inverted x0>x1 or top>bottom bbox values | PR #13483: swap inverted axis pairs |
| Wrong page numbers after batch parsing of large PDFs | Chunk-local page indices not offset to global document page numbers | PR #14385: _to_global_boxes() adds page_from |
| Raw 0–1000 values emitted when page images unavailable | Missing fallback in _line_tag | PR #16283: fall back to captured/default page size |
Reference Map#
| Layer | File / Module | Role |
|---|---|---|
| Backend normalization | rag/flow/parser/pdf_chunk_metadata.py | Canonical format, thumbnail crop, preview restore |
| Frontend highlight builder | web/src/utils/document-util.ts | positions[] → IHighlight[] |
| PDF viewer component | web/src/components/document-preview/pdf-preview.tsx | react-pdf-highlighter wrapper, scroll-to-highlight |
| MinerU parser | deepdoc/parser/mineru_parser.py | 0–1000 → pixel conversion, cross-page table fix |
| DeepDoc Python parser | deepdoc/parser/pdf_parser.py | position_tag encoding, extract_positions() |
| Go PDF parser | internal/deepdoc/parser/pdf/ | DlaScale=3.0 OCR→PDF DPI conversion |