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:
xerxesliu
2025-11-29 14:40:49 +08:00
committed by GitHub
parent c23e88ecd1
commit 876f59d650
2 changed files with 67 additions and 16 deletions

View File

@@ -10,6 +10,7 @@ import {
} from '@ant-design/icons'
import { loggerService } from '@logger'
import { download } from '@renderer/utils/download'
import { convertImageToPng } from '@renderer/utils/image'
import type { ImageProps as AntImageProps } from 'antd'
import { Dropdown, Image as AntImage, Space } from 'antd'
import { Base64 } from 'js-base64'
@@ -33,39 +34,38 @@ const ImageViewer: React.FC<ImageViewerProps> = ({ src, style, ...props }) => {
// 复制图片到剪贴板
const handleCopyImage = async (src: string) => {
try {
let blob: Blob
if (src.startsWith('data:')) {
// 处理 base64 格式的图片
const match = src.match(/^data:(image\/\w+);base64,(.+)$/)
if (!match) throw new Error('Invalid base64 image format')
const mimeType = match[1]
const byteArray = Base64.toUint8Array(match[2])
const blob = new Blob([byteArray], { type: mimeType })
await navigator.clipboard.write([new ClipboardItem({ [mimeType]: blob })])
blob = new Blob([byteArray], { type: mimeType })
} else if (src.startsWith('file://')) {
// 处理本地文件路径
const bytes = await window.api.fs.read(src)
const mimeType = mime.getType(src) || 'application/octet-stream'
const blob = new Blob([bytes], { type: mimeType })
await navigator.clipboard.write([
new ClipboardItem({
[mimeType]: blob
})
])
blob = new Blob([bytes], { type: mimeType })
} else {
// 处理 URL 格式的图片
const response = await fetch(src)
const blob = await response.blob()
await navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob
})
])
blob = await response.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'))
} 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'))
}
}

View File

@@ -566,3 +566,54 @@ export const makeSvgSizeAdaptive = (element: Element): 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
})
}