mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-19 14:41:24 +08:00
fix: resolve copy image failure for JPEG format pictures (#11529)
- Convert all image formats to PNG before writing to clipboard to ensure compatibility - Refactor handleCopyImage to unify image source handling (Base64, File, URL) - Add convertImageToPng utility function using canvas API for robust conversion - Remove fallback logic that attempted to write unsupported JPEG format
This commit is contained in:
parent
c23e88ecd1
commit
876f59d650
@ -10,6 +10,7 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { download } from '@renderer/utils/download'
|
import { download } from '@renderer/utils/download'
|
||||||
|
import { convertImageToPng } from '@renderer/utils/image'
|
||||||
import type { ImageProps as AntImageProps } from 'antd'
|
import type { ImageProps as AntImageProps } from 'antd'
|
||||||
import { Dropdown, Image as AntImage, Space } from 'antd'
|
import { Dropdown, Image as AntImage, Space } from 'antd'
|
||||||
import { Base64 } from 'js-base64'
|
import { Base64 } from 'js-base64'
|
||||||
@ -33,39 +34,38 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, style, ...props }) => {
|
|||||||
// 复制图片到剪贴板
|
// 复制图片到剪贴板
|
||||||
const handleCopyImage = async (src: string) => {
|
const handleCopyImage = async (src: string) => {
|
||||||
try {
|
try {
|
||||||
|
let blob: Blob
|
||||||
|
|
||||||
if (src.startsWith('data:')) {
|
if (src.startsWith('data:')) {
|
||||||
// 处理 base64 格式的图片
|
// 处理 base64 格式的图片
|
||||||
const match = src.match(/^data:(image\/\w+);base64,(.+)$/)
|
const match = src.match(/^data:(image\/\w+);base64,(.+)$/)
|
||||||
if (!match) throw new Error('Invalid base64 image format')
|
if (!match) throw new Error('Invalid base64 image format')
|
||||||
const mimeType = match[1]
|
const mimeType = match[1]
|
||||||
const byteArray = Base64.toUint8Array(match[2])
|
const byteArray = Base64.toUint8Array(match[2])
|
||||||
const blob = new Blob([byteArray], { type: mimeType })
|
blob = new Blob([byteArray], { type: mimeType })
|
||||||
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
|
|
||||||
} else if (src.startsWith('file://')) {
|
} else if (src.startsWith('file://')) {
|
||||||
// 处理本地文件路径
|
// 处理本地文件路径
|
||||||
const bytes = await window.api.fs.read(src)
|
const bytes = await window.api.fs.read(src)
|
||||||
const mimeType = mime.getType(src) || 'application/octet-stream'
|
const mimeType = mime.getType(src) || 'application/octet-stream'
|
||||||
const blob = new Blob([bytes], { type: mimeType })
|
blob = new Blob([bytes], { type: mimeType })
|
||||||
await navigator.clipboard.write([
|
|
||||||
new ClipboardItem({
|
|
||||||
[mimeType]: blob
|
|
||||||
})
|
|
||||||
])
|
|
||||||
} else {
|
} else {
|
||||||
// 处理 URL 格式的图片
|
// 处理 URL 格式的图片
|
||||||
const response = await fetch(src)
|
const response = await fetch(src)
|
||||||
const blob = await response.blob()
|
blob = await response.blob()
|
||||||
|
|
||||||
await navigator.clipboard.write([
|
|
||||||
new ClipboardItem({
|
|
||||||
[blob.type]: blob
|
|
||||||
})
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 统一转换为 PNG 以确保兼容性(剪贴板 API 不支持 JPEG)
|
||||||
|
const pngBlob = await convertImageToPng(blob)
|
||||||
|
|
||||||
|
const item = new ClipboardItem({
|
||||||
|
'image/png': pngBlob
|
||||||
|
})
|
||||||
|
await navigator.clipboard.write([item])
|
||||||
|
|
||||||
window.toast.success(t('message.copy.success'))
|
window.toast.success(t('message.copy.success'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to copy image:', error as Error)
|
const err = error as Error
|
||||||
|
logger.error(`Failed to copy image: ${err.message}`, { stack: err.stack })
|
||||||
window.toast.error(t('message.copy.failed'))
|
window.toast.error(t('message.copy.failed'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -566,3 +566,54 @@ export const makeSvgSizeAdaptive = (element: Element): Element => {
|
|||||||
|
|
||||||
return element
|
return element
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将图片 Blob 转换为 PNG 格式的 Blob
|
||||||
|
* @param blob 原始图片 Blob
|
||||||
|
* @returns Promise<Blob> 转换后的 PNG Blob
|
||||||
|
*/
|
||||||
|
export const convertImageToPng = async (blob: Blob): Promise<Blob> => {
|
||||||
|
if (blob.type === 'image/png') {
|
||||||
|
return blob
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
try {
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = img.width
|
||||||
|
canvas.height = img.height
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
reject(new Error('Failed to get canvas context'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.drawImage(img, 0, 0)
|
||||||
|
canvas.toBlob((pngBlob) => {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
if (pngBlob) {
|
||||||
|
resolve(pngBlob)
|
||||||
|
} else {
|
||||||
|
reject(new Error('Failed to convert image to png'))
|
||||||
|
}
|
||||||
|
}, 'image/png')
|
||||||
|
} catch (error) {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
reject(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = () => {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
reject(new Error('Failed to load image for conversion'))
|
||||||
|
}
|
||||||
|
|
||||||
|
img.src = url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user