feat: enable rendering and download of inline base64-encoded images (#6669)

This commit introduces support for displaying and downloading
inline base64-encoded images (specifically PNG and JPEG formats)
within Markdown content.

Key changes:
- Modified 'urlTransform' in the Markdown component to allow 'data:image/png'
  and 'data:image/jpeg' URLs, enabling their rendering.
- Updated the 'download' utility to handle 'data:' URLs,
  allowing users to save these inline images.

Signed-off-by: Chan Lee <Leetimemp@gmail.com>
This commit is contained in:
Doekin 2025-06-06 00:29:47 +08:00 committed by GitHub
parent dd15b391c5
commit b94140bc26
2 changed files with 31 additions and 14 deletions

View File

@ -12,7 +12,7 @@ import { findCitationInChildren, getCodeBlockId } from '@renderer/utils/markdown
import { isEmpty } from 'lodash'
import { type FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown, { type Components } from 'react-markdown'
import ReactMarkdown, { type Components, defaultUrlTransform } from 'react-markdown'
import rehypeKatex from 'rehype-katex'
// @ts-ignore rehype-mathjax is not typed
import rehypeMathjax from 'rehype-mathjax'
@ -88,6 +88,11 @@ const Markdown: FC<Props> = ({ block }) => {
} as Partial<Components>
}, [onSaveCodeBlock])
const urlTransform = useCallback((value: string) => {
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
return defaultUrlTransform(value)
}, [])
// if (role === 'user' && !renderInputMessageAsMarkdown) {
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
// }
@ -103,6 +108,7 @@ const Markdown: FC<Props> = ({ block }) => {
className="markdown"
components={components}
disallowedElements={DISALLOWED_ELEMENTS}
urlTransform={urlTransform}
remarkRehypeOptions={{
footnoteLabel: t('common.footnotes'),
footnoteLabelTagName: 'h4',

View File

@ -1,20 +1,31 @@
export const download = (url: string, filename?: string) => {
// 处理 file:// 协议
if (url.startsWith('file://')) {
// 处理可直接通过 <a> 标签下载的 URL:
// - 本地文件 ( file:// )
// - 对象 URL ( blob: )
// - 相对安全的内联数据 ( data:image/png, data:image/jpeg )
// (注: 其他 data 类型,如 data:text/html 或 data:image/svg+xml
// 因其潜在安全风险,不在此处理,将由后续 fetch 逻辑处理或被 CSP 阻止。)
const SUPPORTED_PREFIXES = ['file://', 'blob:', 'data:image/png', 'data:image/jpeg']
if (SUPPORTED_PREFIXES.some((prefix) => url.startsWith(prefix))) {
const link = document.createElement('a')
link.href = url
link.download = filename || url.split('/').pop() || 'download'
document.body.appendChild(link)
link.click()
link.remove()
return
}
// 处理 Blob URL
if (url.startsWith('blob:')) {
const link = document.createElement('a')
link.href = url
link.download = filename || `${Date.now()}_diagram.svg`
let resolvedFilename = filename
if (!resolvedFilename) {
if (url.startsWith('file://')) {
const pathname = new URL(url).pathname
resolvedFilename = decodeURIComponent(pathname.substring(pathname.lastIndexOf('/') + 1))
} else if (url.startsWith('blob:')) {
resolvedFilename = `${Date.now()}_diagram.svg`
} else if (url.startsWith('data:')) {
const mimeMatch = url.match(/^data:([^;,]+)[;,]/)
const mimeType = mimeMatch && mimeMatch[1]
const extension = getExtensionFromMimeType(mimeType)
resolvedFilename = `${Date.now()}_download${extension}`
} else resolvedFilename = 'download'
}
link.download = resolvedFilename
document.body.appendChild(link)
link.click()
link.remove()