fix(docs-generator): Add multi-language font support and reportlab dependency

- Add reportlab>=4.4.1 to dependencies (pyproject.toml)
- Add fonts-freefont-ttf and fonts-noto-cjk to Dockerfile for Unicode support
- Implement automatic CJK font detection and switching:
  - Latin content uses user-selected font (Helvetica, Times-Roman, Courier)
  - CJK/Arabic/Hebrew/Thai/Hindi content auto-switches to STSong-Light CID font
- Add documentation about automatic font switching behavior
- Fix frontend null safety in use-selelct-filters.ts
- Add DocsGenerator component to agent constants

Fixes missing glyphs (black squares) issue for Chinese, Japanese, Korean,
Arabic, Hebrew, Thai, and Hindi characters in generated PDFs.
This commit is contained in:
PentaFrame-Development 2025-12-11 11:29:22 +01:00
parent a7b49c0c45
commit fb3fedbbcf
7 changed files with 271 additions and 50 deletions

View file

@ -52,7 +52,8 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \
apt install -y nginx unzip curl wget git vim less && \ apt install -y nginx unzip curl wget git vim less && \
apt install -y ghostscript && \ apt install -y ghostscript && \
apt install -y pandoc && \ apt install -y pandoc && \
apt install -y texlive apt install -y texlive && \
apt install -y fonts-freefont-ttf fonts-noto-cjk
# Install uv # Install uv
RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \ RUN --mount=type=bind,from=infiniflow/ragflow_deps:latest,source=/,target=/deps \

View file

@ -3,6 +3,7 @@ import json
import os import os
import re import re
import base64 import base64
import unicodedata
from datetime import datetime from datetime import datetime
from abc import ABC from abc import ABC
from io import BytesIO from io import BytesIO
@ -15,6 +16,9 @@ from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, PageBreak, LongTable from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Image, Table, TableStyle, PageBreak, LongTable
from reportlab.lib import colors from reportlab.lib import colors
from reportlab.pdfgen import canvas as pdf_canvas from reportlab.pdfgen import canvas as pdf_canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.pdfbase.cidfonts import UnicodeCIDFont, CIDFont
from jinja2 import Template as Jinja2Template from jinja2 import Template as Jinja2Template
from agent.component.base import ComponentParamBase from agent.component.base import ComponentParamBase
@ -98,6 +102,145 @@ class PDFGeneratorParam(ComponentParamBase):
class PDFGenerator(Message, ABC): class PDFGenerator(Message, ABC):
component_name = "PDFGenerator" component_name = "PDFGenerator"
# Track if Unicode fonts have been registered
_unicode_fonts_registered = False
_unicode_font_name = None
_unicode_font_bold_name = None
@classmethod
def _reset_font_cache(cls):
"""Reset font registration cache - useful for testing"""
cls._unicode_fonts_registered = False
cls._unicode_font_name = None
cls._unicode_font_bold_name = None
@classmethod
def _register_unicode_fonts(cls):
"""Register Unicode-compatible fonts for multi-language support.
Uses CID fonts (STSong-Light) for reliable CJK rendering as TTF fonts
have issues with glyph mapping in some ReportLab versions.
"""
# If already registered successfully, return True
if cls._unicode_fonts_registered and cls._unicode_font_name is not None:
return True
# Reset and try again if previous registration failed
cls._unicode_fonts_registered = True
cls._unicode_font_name = None
cls._unicode_font_bold_name = None
# Use CID fonts for reliable CJK support
# These are built into ReportLab and work reliably across all platforms
cid_fonts = [
'STSong-Light', # Simplified Chinese
'HeiseiMin-W3', # Japanese
'HYSMyeongJo-Medium', # Korean
]
for cid_font in cid_fonts:
try:
pdfmetrics.registerFont(UnicodeCIDFont(cid_font))
cls._unicode_font_name = cid_font
cls._unicode_font_bold_name = cid_font # CID fonts don't have bold variants
print(f"Registered CID font: {cid_font}")
break
except Exception as e:
print(f"Failed to register CID font {cid_font}: {e}")
continue
# If CID fonts fail, try TTF fonts as fallback
if not cls._unicode_font_name:
font_paths = [
'/usr/share/fonts/truetype/freefont/FreeSans.ttf',
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
]
for font_path in font_paths:
if os.path.exists(font_path):
try:
pdfmetrics.registerFont(TTFont('UnicodeFont', font_path))
cls._unicode_font_name = 'UnicodeFont'
cls._unicode_font_bold_name = 'UnicodeFont'
print(f"Registered TTF font from: {font_path}")
# Register font family
from reportlab.pdfbase.pdfmetrics import registerFontFamily
registerFontFamily('UnicodeFont', normal='UnicodeFont', bold='UnicodeFont')
break
except Exception as e:
print(f"Failed to register TTF font {font_path}: {e}")
continue
return cls._unicode_font_name is not None
@staticmethod
def _needs_unicode_font(text: str) -> bool:
"""Check if text contains CJK or other complex scripts that need special fonts.
Standard PDF fonts (Helvetica, Times, Courier) support:
- Basic Latin, Extended Latin, Cyrillic, Greek
CID fonts are needed for:
- CJK (Chinese, Japanese, Korean)
- Arabic, Hebrew (RTL scripts)
- Thai, Hindi, and other Indic scripts
"""
if not text:
return False
for char in text:
code = ord(char)
# CJK Unified Ideographs and related ranges
if 0x4E00 <= code <= 0x9FFF: # CJK Unified Ideographs
return True
if 0x3400 <= code <= 0x4DBF: # CJK Extension A
return True
if 0x3000 <= code <= 0x303F: # CJK Symbols and Punctuation
return True
if 0x3040 <= code <= 0x309F: # Hiragana
return True
if 0x30A0 <= code <= 0x30FF: # Katakana
return True
if 0xAC00 <= code <= 0xD7AF: # Hangul Syllables
return True
if 0x1100 <= code <= 0x11FF: # Hangul Jamo
return True
# Arabic and Hebrew (RTL scripts)
if 0x0600 <= code <= 0x06FF: # Arabic
return True
if 0x0590 <= code <= 0x05FF: # Hebrew
return True
# Indic scripts
if 0x0900 <= code <= 0x097F: # Devanagari (Hindi)
return True
if 0x0E00 <= code <= 0x0E7F: # Thai
return True
return False
def _get_font_for_content(self, content: str) -> tuple:
"""Get appropriate font based on content, returns (regular_font, bold_font)"""
if self._needs_unicode_font(content):
if self._register_unicode_fonts() and self._unicode_font_name:
return (self._unicode_font_name, self._unicode_font_bold_name or self._unicode_font_name)
else:
print("Warning: Content contains non-Latin characters but no Unicode font available")
# Fall back to configured font
return (self._param.font_family, self._get_bold_font_name())
def _get_active_font(self) -> str:
"""Get the currently active font (Unicode or configured)"""
return getattr(self, '_active_font', self._param.font_family)
def _get_active_bold_font(self) -> str:
"""Get the currently active bold font (Unicode or configured)"""
return getattr(self, '_active_bold_font', self._get_bold_font_name())
def _get_bold_font_name(self) -> str: def _get_bold_font_name(self) -> str:
"""Get the correct bold variant of the current font family""" """Get the correct bold variant of the current font family"""
font_map = { font_map = {
@ -136,8 +279,8 @@ class PDFGenerator(Message, ABC):
title = self._param.title or "" title = self._param.title or ""
subtitle = self._param.subtitle or "" subtitle = self._param.subtitle or ""
# Add debug logging # Log PDF generation start
print(f"Starting PDF generation for title: {title}") print(f"Starting PDF generation for title: {title}, content length: {len(content)} chars")
# Resolve variable references in content using canvas # Resolve variable references in content using canvas
if content and self._canvas.is_reff(content): if content and self._canvas.is_reff(content):
@ -205,9 +348,6 @@ class PDFGenerator(Message, ABC):
if not content: if not content:
content = kwargs.get("content", "") content = kwargs.get("content", "")
# Add debug logging
print(f"Starting PDF generation with content length: {len(content)} characters")
# Generate document based on format # Generate document based on format
try: try:
output_format = self._param.output_format or "pdf" output_format = self._param.output_format or "pdf"
@ -317,7 +457,15 @@ class PDFGenerator(Message, ABC):
# Build story (content elements) # Build story (content elements)
story = [] story = []
styles = self._create_styles() # Combine all text content for Unicode font detection
all_text = f"{title} {subtitle} {content}"
# IMPORTANT: Register Unicode fonts BEFORE creating any styles or Paragraphs
# This ensures the font family is available for ReportLab's HTML parser
if self._needs_unicode_font(all_text):
self._register_unicode_fonts()
styles = self._create_styles(all_text)
# Add logo if provided # Add logo if provided
if self._param.logo_image: if self._param.logo_image:
@ -397,13 +545,41 @@ class PDFGenerator(Message, ABC):
except Exception as close_error: except Exception as close_error:
print(f"Error closing buffer: {close_error}") print(f"Error closing buffer: {close_error}")
def _create_styles(self): def _create_styles(self, content: str = ""):
"""Create custom paragraph styles""" """Create custom paragraph styles with Unicode font support if needed"""
# Check if content contains CJK characters that need special fonts
needs_cjk = self._needs_unicode_font(content)
if needs_cjk:
# Use CID fonts for CJK content
if self._register_unicode_fonts() and self._unicode_font_name:
regular_font = self._unicode_font_name
bold_font = self._unicode_font_bold_name or self._unicode_font_name
print(f"Using CID font for CJK content: {regular_font}")
else:
# Fall back to configured font if CID fonts unavailable
regular_font = self._param.font_family
bold_font = self._get_bold_font_name()
print(f"Warning: CJK content detected but no CID font available, using {regular_font}")
else:
# Use user-selected font for Latin-only content
regular_font = self._param.font_family
bold_font = self._get_bold_font_name()
print(f"Using configured font: {regular_font}")
# Store active fonts as instance variables for use in other methods
self._active_font = regular_font
self._active_bold_font = bold_font
# Get fresh style sheet
styles = getSampleStyleSheet() styles = getSampleStyleSheet()
# Helper function to get the correct bold font name # Helper function to get the correct bold font name
def get_bold_font(font_family): def get_bold_font(font_family):
"""Get the correct bold variant of a font family""" """Get the correct bold variant of a font family"""
# If using Unicode font, return the Unicode bold
if font_family in ('UnicodeFont', self._unicode_font_name):
return bold_font
font_map = { font_map = {
'Helvetica': 'Helvetica-Bold', 'Helvetica': 'Helvetica-Bold',
'Times-Roman': 'Times-Bold', 'Times-Roman': 'Times-Bold',
@ -413,6 +589,10 @@ class PDFGenerator(Message, ABC):
return font_family return font_family
return font_map.get(font_family, 'Helvetica-Bold') return font_map.get(font_family, 'Helvetica-Bold')
# Use detected font instead of configured font for non-Latin content
active_font = regular_font
active_bold_font = bold_font
# Helper function to add or update style # Helper function to add or update style
def add_or_update_style(name, **kwargs): def add_or_update_style(name, **kwargs):
if name in styles: if name in styles:
@ -424,13 +604,23 @@ class PDFGenerator(Message, ABC):
# Add new style # Add new style
styles.add(ParagraphStyle(name=name, **kwargs)) styles.add(ParagraphStyle(name=name, **kwargs))
# IMPORTANT: Update base styles to use Unicode font for non-Latin content
# This ensures ALL text uses the correct font, not just our custom styles
add_or_update_style('Normal', fontName=active_font)
add_or_update_style('BodyText', fontName=active_font)
add_or_update_style('Bullet', fontName=active_font)
add_or_update_style('Heading1', fontName=active_bold_font)
add_or_update_style('Heading2', fontName=active_bold_font)
add_or_update_style('Heading3', fontName=active_bold_font)
add_or_update_style('Title', fontName=active_bold_font)
# Title style # Title style
add_or_update_style( add_or_update_style(
'PDFTitle', 'PDFTitle',
parent=styles['Heading1'], parent=styles['Heading1'],
fontSize=self._param.title_font_size, fontSize=self._param.title_font_size,
textColor=colors.HexColor(self._param.title_color), textColor=colors.HexColor(self._param.title_color),
fontName=get_bold_font(self._param.font_family), fontName=active_bold_font,
alignment=TA_CENTER, alignment=TA_CENTER,
spaceAfter=12 spaceAfter=12
) )
@ -441,7 +631,7 @@ class PDFGenerator(Message, ABC):
parent=styles['Heading2'], parent=styles['Heading2'],
fontSize=self._param.heading2_font_size, fontSize=self._param.heading2_font_size,
textColor=colors.HexColor(self._param.text_color), textColor=colors.HexColor(self._param.text_color),
fontName=self._param.font_family, fontName=active_font,
alignment=TA_CENTER, alignment=TA_CENTER,
spaceAfter=12 spaceAfter=12
) )
@ -451,7 +641,7 @@ class PDFGenerator(Message, ABC):
'CustomHeading1', 'CustomHeading1',
parent=styles['Heading1'], parent=styles['Heading1'],
fontSize=self._param.heading1_font_size, fontSize=self._param.heading1_font_size,
fontName=get_bold_font(self._param.font_family), fontName=active_bold_font,
textColor=colors.HexColor(self._param.text_color), textColor=colors.HexColor(self._param.text_color),
spaceAfter=12, spaceAfter=12,
spaceBefore=12 spaceBefore=12
@ -461,7 +651,7 @@ class PDFGenerator(Message, ABC):
'CustomHeading2', 'CustomHeading2',
parent=styles['Heading2'], parent=styles['Heading2'],
fontSize=self._param.heading2_font_size, fontSize=self._param.heading2_font_size,
fontName=get_bold_font(self._param.font_family), fontName=active_bold_font,
textColor=colors.HexColor(self._param.text_color), textColor=colors.HexColor(self._param.text_color),
spaceAfter=10, spaceAfter=10,
spaceBefore=10 spaceBefore=10
@ -471,7 +661,7 @@ class PDFGenerator(Message, ABC):
'CustomHeading3', 'CustomHeading3',
parent=styles['Heading3'], parent=styles['Heading3'],
fontSize=self._param.heading3_font_size, fontSize=self._param.heading3_font_size,
fontName=get_bold_font(self._param.font_family), fontName=active_bold_font,
textColor=colors.HexColor(self._param.text_color), textColor=colors.HexColor(self._param.text_color),
spaceAfter=8, spaceAfter=8,
spaceBefore=8 spaceBefore=8
@ -482,7 +672,7 @@ class PDFGenerator(Message, ABC):
'CustomBody', 'CustomBody',
parent=styles['BodyText'], parent=styles['BodyText'],
fontSize=self._param.font_size, fontSize=self._param.font_size,
fontName=self._param.font_family, fontName=active_font,
textColor=colors.HexColor(self._param.text_color), textColor=colors.HexColor(self._param.text_color),
leading=self._param.font_size * self._param.line_spacing, leading=self._param.font_size * self._param.line_spacing,
alignment=TA_JUSTIFY alignment=TA_JUSTIFY
@ -493,13 +683,13 @@ class PDFGenerator(Message, ABC):
'CustomBullet', 'CustomBullet',
parent=styles['BodyText'], parent=styles['BodyText'],
fontSize=self._param.font_size, fontSize=self._param.font_size,
fontName=self._param.font_family, fontName=active_font,
textColor=colors.HexColor(self._param.text_color), textColor=colors.HexColor(self._param.text_color),
leftIndent=20, leftIndent=20,
bulletIndent=10 bulletIndent=10
) )
# Code style # Code style (keep Courier for code blocks)
add_or_update_style( add_or_update_style(
'PDFCode', 'PDFCode',
parent=styles.get('Code', styles['Normal']), parent=styles.get('Code', styles['Normal']),
@ -516,7 +706,7 @@ class PDFGenerator(Message, ABC):
'Italic', 'Italic',
parent=styles['Normal'], parent=styles['Normal'],
fontSize=self._param.font_size, fontSize=self._param.font_size,
fontName=self._param.font_family, fontName=active_font,
textColor=colors.HexColor(self._param.text_color) textColor=colors.HexColor(self._param.text_color)
) )
@ -571,7 +761,8 @@ class PDFGenerator(Message, ABC):
bullet_items = [] bullet_items = []
while i < len(lines) and (lines[i].strip().startswith('- ') or lines[i].strip().startswith('* ')): while i < len(lines) and (lines[i].strip().startswith('- ') or lines[i].strip().startswith('* ')):
item_text = lines[i].strip()[2:].strip() item_text = lines[i].strip()[2:].strip()
bullet_items.append(f"{self._format_inline(item_text)}") formatted = self._format_inline(item_text)
bullet_items.append(f"{formatted}")
i += 1 i += 1
for item in bullet_items: for item in bullet_items:
elements.append(Paragraph(item, styles['CustomBullet'])) elements.append(Paragraph(item, styles['CustomBullet']))
@ -754,7 +945,7 @@ class PDFGenerator(Message, ABC):
'TableHeader', 'TableHeader',
parent=styles['Normal'], parent=styles['Normal'],
fontSize=self._param.font_size, fontSize=self._param.font_size,
fontName=get_bold_font(self._param.font_family), fontName=self._get_active_bold_font(),
textColor=colors.whitesmoke, textColor=colors.whitesmoke,
alignment=TA_CENTER, alignment=TA_CENTER,
leading=self._param.font_size * 1.2, leading=self._param.font_size * 1.2,
@ -766,7 +957,7 @@ class PDFGenerator(Message, ABC):
'TableCell', 'TableCell',
parent=styles['Normal'], parent=styles['Normal'],
fontSize=font_size, fontSize=font_size,
fontName=self._param.font_family, fontName=self._get_active_font(),
textColor=colors.black, textColor=colors.black,
alignment=TA_LEFT, alignment=TA_LEFT,
leading=font_size * 1.15, leading=font_size * 1.15,
@ -795,7 +986,7 @@ class PDFGenerator(Message, ABC):
'TableHeader', 'TableHeader',
parent=styles['Normal'], parent=styles['Normal'],
fontSize=base_font_size + 1, fontSize=base_font_size + 1,
fontName=self._get_bold_font_name(), fontName=self._get_active_bold_font(),
textColor=colors.HexColor('#2c3e50'), textColor=colors.HexColor('#2c3e50'),
spaceAfter=6, spaceAfter=6,
backColor=colors.HexColor('#f8f9fa'), backColor=colors.HexColor('#f8f9fa'),
@ -810,7 +1001,7 @@ class PDFGenerator(Message, ABC):
'TableBody', 'TableBody',
parent=styles['Normal'], parent=styles['Normal'],
fontSize=base_font_size, fontSize=base_font_size,
fontName=getattr(self._param, 'font_family', 'Helvetica'), fontName=self._get_active_font(),
textColor=colors.HexColor(getattr(self._param, 'text_color', '#000000')), textColor=colors.HexColor(getattr(self._param, 'text_color', '#000000')),
spaceAfter=6, spaceAfter=6,
leading=base_font_size * 1.2 leading=base_font_size * 1.2
@ -820,7 +1011,7 @@ class PDFGenerator(Message, ABC):
label_style = ParagraphStyle( label_style = ParagraphStyle(
'LabelStyle', 'LabelStyle',
parent=body_style, parent=body_style,
fontName=self._get_bold_font_name(), fontName=self._get_active_bold_font(),
textColor=colors.HexColor('#2c3e50'), textColor=colors.HexColor('#2c3e50'),
fontSize=base_font_size, fontSize=base_font_size,
spaceAfter=4, spaceAfter=4,
@ -1121,7 +1312,8 @@ class PDFGenerator(Message, ABC):
font_size = self._param.font_size - 1 if row_idx > 0 else self._param.font_size font_size = self._param.font_size - 1 if row_idx > 0 else self._param.font_size
try: try:
style = self._get_cell_style(row_idx, is_header=(row_idx == 0), font_size=font_size) style = self._get_cell_style(row_idx, is_header=(row_idx == 0), font_size=font_size)
processed_row.append(Paragraph(self._escape_html(cell_text), style)) escaped_text = self._escape_html(cell_text)
processed_row.append(Paragraph(escaped_text, style))
except Exception as e: except Exception as e:
processed_row.append(self._escape_html(cell_text)) processed_row.append(self._escape_html(cell_text))
@ -1146,7 +1338,7 @@ class PDFGenerator(Message, ABC):
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')), # Darker header ('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#2c3e50')), # Darker header
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
('ALIGN', (0, 0), (-1, 0), 'CENTER'), ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
('FONTNAME', (0, 0), (-1, 0), self._get_bold_font_name()), ('FONTNAME', (0, 0), (-1, 0), self._get_active_bold_font()),
('FONTSIZE', (0, 0), (-1, -1), self._param.font_size - 1), ('FONTSIZE', (0, 0), (-1, -1), self._param.font_size - 1),
('BOTTOMPADDING', (0, 0), (-1, 0), 12), ('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#f8f9fa')), # Lighter background ('BACKGROUND', (0, 1), (-1, -1), colors.HexColor('#f8f9fa')), # Lighter background
@ -1232,9 +1424,12 @@ class PDFGenerator(Message, ABC):
"""Add header, footer, page numbers, watermark""" """Add header, footer, page numbers, watermark"""
canvas.saveState() canvas.saveState()
# Get active font for decorations
active_font = self._get_active_font()
# Add watermark # Add watermark
if self._param.watermark_text: if self._param.watermark_text:
canvas.setFont(self._param.font_family, 60) canvas.setFont(active_font, 60)
canvas.setFillColorRGB(0.9, 0.9, 0.9, alpha=0.3) canvas.setFillColorRGB(0.9, 0.9, 0.9, alpha=0.3)
canvas.saveState() canvas.saveState()
canvas.translate(doc.pagesize[0] / 2, doc.pagesize[1] / 2) canvas.translate(doc.pagesize[0] / 2, doc.pagesize[1] / 2)
@ -1244,13 +1439,13 @@ class PDFGenerator(Message, ABC):
# Add header # Add header
if self._param.header_text: if self._param.header_text:
canvas.setFont(self._param.font_family, 9) canvas.setFont(active_font, 9)
canvas.setFillColorRGB(0.5, 0.5, 0.5) canvas.setFillColorRGB(0.5, 0.5, 0.5)
canvas.drawString(doc.leftMargin, doc.pagesize[1] - 0.5 * inch, self._param.header_text) canvas.drawString(doc.leftMargin, doc.pagesize[1] - 0.5 * inch, self._param.header_text)
# Add footer # Add footer
if self._param.footer_text: if self._param.footer_text:
canvas.setFont(self._param.font_family, 9) canvas.setFont(active_font, 9)
canvas.setFillColorRGB(0.5, 0.5, 0.5) canvas.setFillColorRGB(0.5, 0.5, 0.5)
canvas.drawString(doc.leftMargin, 0.5 * inch, self._param.footer_text) canvas.drawString(doc.leftMargin, 0.5 * inch, self._param.footer_text)
@ -1258,7 +1453,7 @@ class PDFGenerator(Message, ABC):
if self._param.add_page_numbers: if self._param.add_page_numbers:
page_num = canvas.getPageNumber() page_num = canvas.getPageNumber()
text = f"Page {page_num}" text = f"Page {page_num}"
canvas.setFont(self._param.font_family, 9) canvas.setFont(active_font, 9)
canvas.setFillColorRGB(0.5, 0.5, 0.5) canvas.setFillColorRGB(0.5, 0.5, 0.5)
canvas.drawRightString(doc.pagesize[0] - doc.rightMargin, 0.5 * inch, text) canvas.drawRightString(doc.pagesize[0] - doc.rightMargin, 0.5 * inch, text)

View file

@ -71,6 +71,10 @@ The font used throughout the document:
- **Times-Roman** - **Times-Roman**
- **Courier** - **Courier**
:::tip NOTE
When the document contains CJK (Chinese, Japanese, Korean) or other non-Latin characters, the system automatically switches to a compatible Unicode font (STSong-Light) to ensure proper rendering. The selected font family is used for Latin-only content.
:::
### Font size ### Font size
The base font size in points. Defaults to `12`. The base font size in points. Defaults to `12`.
@ -132,3 +136,13 @@ To display a download button in the chat, add a **Message** component after the
1. Connect the **Docs Generator** output to a **Message** component. 1. Connect the **Docs Generator** output to a **Message** component.
2. In the **Message** component's content field, type `/` and select `{Docs Generator_0@download}`. 2. In the **Message** component's content field, type `/` and select `{Docs Generator_0@download}`.
3. When the agent runs, a download button will appear in the chat, allowing users to download the generated document. 3. When the agent runs, a download button will appear in the chat, allowing users to download the generated document.
## Multi-language support
The **Docs Generator** automatically detects non-Latin characters (Chinese, Japanese, Korean, Arabic, Hebrew, Cyrillic, etc.) and uses appropriate Unicode fonts when available on the server.
:::tip NOTE
For full multi-language support, ensure Unicode fonts are installed on the RAGFlow server:
- **Linux**: `fonts-freefont-ttf`, `fonts-noto-cjk`, or `fonts-droid-fallback`
- **Docker**: Add font packages to the Dockerfile if needed
:::

View file

@ -154,6 +154,8 @@ dependencies = [
"exceptiongroup>=1.3.0,<2.0.0", "exceptiongroup>=1.3.0,<2.0.0",
"ffmpeg-python>=0.2.0", "ffmpeg-python>=0.2.0",
"imageio-ffmpeg>=0.6.0", "imageio-ffmpeg>=0.6.0",
"reportlab>=4.4.1",
"jinja2>=3.1.0",
] ]
[dependency-groups] [dependency-groups]

38
uv.lock generated
View file

@ -1,5 +1,5 @@
version = 1 version = 1
revision = 3 revision = 2
requires-python = ">=3.12, <3.15" requires-python = ">=3.12, <3.15"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'darwin'", "python_full_version >= '3.14' and sys_platform == 'darwin'",
@ -3257,7 +3257,7 @@ wheels = [
[[package]] [[package]]
name = "jupyter-client" name = "jupyter-client"
version = "8.6.3" version = "8.7.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [ dependencies = [
{ name = "jupyter-core" }, { name = "jupyter-core" },
@ -3266,9 +3266,9 @@ dependencies = [
{ name = "tornado" }, { name = "tornado" },
{ name = "traitlets" }, { name = "traitlets" },
] ]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" }
wheels = [ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" },
] ]
[[package]] [[package]]
@ -5955,6 +5955,7 @@ dependencies = [
{ name = "infinity-emb" }, { name = "infinity-emb" },
{ name = "infinity-sdk" }, { name = "infinity-sdk" },
{ name = "itsdangerous" }, { name = "itsdangerous" },
{ name = "jinja2" },
{ name = "jira" }, { name = "jira" },
{ name = "json-repair" }, { name = "json-repair" },
{ name = "langfuse" }, { name = "langfuse" },
@ -6012,6 +6013,7 @@ dependencies = [
{ name = "ranx" }, { name = "ranx" },
{ name = "readability-lxml" }, { name = "readability-lxml" },
{ name = "replicate" }, { name = "replicate" },
{ name = "reportlab" },
{ name = "requests" }, { name = "requests" },
{ name = "roman-numbers" }, { name = "roman-numbers" },
{ name = "ruamel-base" }, { name = "ruamel-base" },
@ -6122,6 +6124,7 @@ requires-dist = [
{ name = "infinity-emb", specifier = ">=0.0.66,<0.0.67" }, { name = "infinity-emb", specifier = ">=0.0.66,<0.0.67" },
{ name = "infinity-sdk", specifier = "==0.6.11" }, { name = "infinity-sdk", specifier = "==0.6.11" },
{ name = "itsdangerous", specifier = "==2.1.2" }, { name = "itsdangerous", specifier = "==2.1.2" },
{ name = "jinja2", specifier = ">=3.1.0" },
{ name = "jira", specifier = "==3.10.5" }, { name = "jira", specifier = "==3.10.5" },
{ name = "json-repair", specifier = "==0.35.0" }, { name = "json-repair", specifier = "==0.35.0" },
{ name = "langfuse", specifier = ">=2.60.0" }, { name = "langfuse", specifier = ">=2.60.0" },
@ -6179,6 +6182,7 @@ requires-dist = [
{ name = "ranx", specifier = "==0.3.20" }, { name = "ranx", specifier = "==0.3.20" },
{ name = "readability-lxml", specifier = ">=0.8.4,<1.0.0" }, { name = "readability-lxml", specifier = ">=0.8.4,<1.0.0" },
{ name = "replicate", specifier = "==0.31.0" }, { name = "replicate", specifier = "==0.31.0" },
{ name = "reportlab", specifier = ">=4.4.1" },
{ name = "requests", specifier = ">=2.32.3,<3.0.0" }, { name = "requests", specifier = ">=2.32.3,<3.0.0" },
{ name = "roman-numbers", specifier = "==1.0.2" }, { name = "roman-numbers", specifier = "==1.0.2" },
{ name = "ruamel-base", specifier = "==1.0.0" }, { name = "ruamel-base", specifier = "==1.0.0" },
@ -7383,21 +7387,21 @@ wheels = [
[[package]] [[package]]
name = "tornado" name = "tornado"
version = "6.5.2" version = "6.5.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" } sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/2e/3d22d478f27cb4b41edd4db7f10cd7846d0a28ea443342de3dba97035166/tornado-6.5.3.tar.gz", hash = "sha256:16abdeb0211796ffc73765bc0a20119712d68afeeaf93d1a3f2edf6b3aee8d5a", size = 513348, upload-time = "2025-12-11T04:16:42.225Z" }
wheels = [ wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/e9/bf22f66e1d5d112c0617974b5ce86666683b32c09b355dfcd59f8d5c8ef6/tornado-6.5.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2dd7d7e8d3e4635447a8afd4987951e3d4e8d1fb9ad1908c54c4002aabab0520", size = 443860, upload-time = "2025-12-11T04:16:26.638Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/9c/594b631f0b8dc5977080c7093d1e96f1377c10552577d2c31bb0208c9362/tornado-6.5.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5977a396f83496657779f59a48c38096ef01edfe4f42f1c0634b791dde8165d0", size = 442118, upload-time = "2025-12-11T04:16:28.32Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/f6/685b869f5b5b9d9547571be838c6106172082751696355b60fc32a4988ed/tornado-6.5.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f72ac800be2ac73ddc1504f7aa21069a4137e8d70c387172c063d363d04f2208", size = 445700, upload-time = "2025-12-11T04:16:29.64Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/4c/f0d19edf24912b7f21ae5e941f7798d132ad4d9b71441c1e70917a297265/tornado-6.5.3-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c43c4fc4f5419c6561cfb8b884a8f6db7b142787d47821e1a0e1296253458265", size = 445041, upload-time = "2025-12-11T04:16:30.799Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/2b/e02da94f4a4aef2bb3b923c838ef284a77548a5f06bac2a8682b36b4eead/tornado-6.5.3-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de8b3fed4b3afb65d542d7702ac8767b567e240f6a43020be8eaef59328f117b", size = 445270, upload-time = "2025-12-11T04:16:32.316Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/e2/7a7535d23133443552719dba526dacbb7415f980157da9f14950ddb88ad6/tornado-6.5.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dbc4b4c32245b952566e17a20d5c1648fbed0e16aec3fc7e19f3974b36e0e47c", size = 445957, upload-time = "2025-12-11T04:16:33.913Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/1f/9ff92eca81ff17a86286ec440dcd5eab0400326eb81761aa9a4eecb1ffb9/tornado-6.5.3-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:db238e8a174b4bfd0d0238b8cfcff1c14aebb4e2fcdafbf0ea5da3b81caceb4c", size = 445371, upload-time = "2025-12-11T04:16:35.093Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/b1/1d03ae4526a393b0b839472a844397337f03c7f3a1e6b5c82241f0e18281/tornado-6.5.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:892595c100cd9b53a768cbfc109dfc55dec884afe2de5290611a566078d9692d", size = 445348, upload-time = "2025-12-11T04:16:36.679Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/7d/7c181feadc8941f418d0d26c3790ee34ffa4bd0a294bc5201d44ebd19c1e/tornado-6.5.3-cp39-abi3-win32.whl", hash = "sha256:88141456525fe291e47bbe1ba3ffb7982549329f09b4299a56813923af2bd197", size = 446433, upload-time = "2025-12-11T04:16:38.332Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/98/4f7f938606e21d0baea8c6c39a7c8e95bdf8e50b0595b1bb6f0de2af7a6e/tornado-6.5.3-cp39-abi3-win_amd64.whl", hash = "sha256:ba4b513d221cc7f795a532c1e296f36bcf6a60e54b15efd3f092889458c69af1", size = 446842, upload-time = "2025-12-11T04:16:39.867Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" }, { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/27/0e3fca4c4edf33fb6ee079e784c63961cd816971a45e5e4cacebe794158d/tornado-6.5.3-cp39-abi3-win_arm64.whl", hash = "sha256:278c54d262911365075dd45e0b6314308c74badd6ff9a54490e7daccdd5ed0ea", size = 445863, upload-time = "2025-12-11T04:16:41.099Z" },
] ]
[[package]] [[package]]

View file

@ -996,6 +996,7 @@ export const initialPDFGeneratorValues = {
success: { type: 'boolean', value: false }, success: { type: 'boolean', value: false },
}, },
}; };
export enum WebhookMethod { export enum WebhookMethod {
Post = 'POST', Post = 'POST',
Get = 'GET', Get = 'GET',

View file

@ -7,11 +7,15 @@ export function useSelectFilters() {
const { data } = useFetchAgentList({}); const { data } = useFetchAgentList({});
const canvasCategory = useMemo(() => { const canvasCategory = useMemo(() => {
return groupListByType(data.canvas, 'canvas_category', 'canvas_category'); return groupListByType(
}, [data.canvas]); data?.canvas ?? [],
'canvas_category',
'canvas_category',
);
}, [data?.canvas]);
const filters: FilterCollection[] = [ const filters: FilterCollection[] = [
buildOwnersFilter(data.canvas), buildOwnersFilter(data?.canvas ?? []),
{ {
field: 'canvasCategory', field: 'canvasCategory',
list: canvasCategory, list: canvasCategory,