[backport core/1.33] fix: handle Unicode characters in clipboard copy/paste and add Paste menu option (#7246)

Backport of #7103 to `core/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7246-backport-core-1-33-fix-handle-Unicode-characters-in-clipboard-copy-paste-and-add-Paste-2c36d73d365081d79d8de04456bdf9d8)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Comfy Org PR Bot
2025-12-09 09:13:24 +09:00
committed by GitHub
parent 4231514baf
commit 4ed7c29f9a
5 changed files with 124 additions and 5 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -23,10 +23,16 @@ export const useCopy = () => {
const canvas = canvasStore.canvas
if (canvas?.selectedItems) {
const serializedData = canvas.copyToClipboard()
// Use TextEncoder to handle Unicode characters properly
const base64Data = btoa(
String.fromCharCode(
...Array.from(new TextEncoder().encode(serializedData))
)
)
// clearData doesn't remove images from clipboard
e.clipboardData?.setData(
'text/html',
clipboardHTMLWrapper.join(btoa(serializedData))
clipboardHTMLWrapper.join(base64Data)
)
e.preventDefault()
e.stopImmediatePropagation()

View File

@@ -14,9 +14,11 @@ function pasteClipboardItems(data: DataTransfer): boolean {
const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1]
if (!match) return false
try {
useCanvasStore()
.getCanvas()
._deserializeItems(JSON.parse(atob(match)), {})
// Decode UTF-8 safe base64
const binaryString = atob(match)
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
const decodedData = new TextDecoder().decode(bytes)
useCanvasStore().getCanvas()._deserializeItems(JSON.parse(decodedData), {})
return true
} catch (err) {
console.error(err)

View File

@@ -8031,7 +8031,13 @@ export class LGraphCanvas
has_submenu: true,
callback: LGraphCanvas.onMenuAdd
},
{ content: 'Add Group', callback: LGraphCanvas.onGroupAdd }
{ content: 'Add Group', callback: LGraphCanvas.onGroupAdd },
{
content: 'Paste',
callback: () => {
this.pasteFromClipboard()
}
}
// { content: "Arrange", callback: that.graph.arrange },
// {content:"Collapse All", callback: LGraphCanvas.onMenuCollapseAll }
]

View File

@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest'
/**
* Encodes a UTF-8 string to base64 (same logic as useCopy.ts)
*/
function encodeClipboardData(data: string): string {
return btoa(
String.fromCharCode(...Array.from(new TextEncoder().encode(data)))
)
}
/**
* Decodes base64 to UTF-8 string (same logic as usePaste.ts)
*/
function decodeClipboardData(base64: string): string {
const binaryString = atob(base64)
const bytes = Uint8Array.from(binaryString, (c) => c.charCodeAt(0))
return new TextDecoder().decode(bytes)
}
describe('Clipboard UTF-8 base64 encoding/decoding', () => {
it('should handle ASCII-only strings', () => {
const original = '{"nodes":[{"id":1,"type":"LoadImage"}]}'
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle Chinese characters in localized_name', () => {
const original =
'{"nodes":[{"id":1,"type":"LoadImage","localized_name":"图像"}]}'
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle Japanese characters', () => {
const original = '{"localized_name":"画像を読み込む"}'
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle Korean characters', () => {
const original = '{"localized_name":"이미지 불러오기"}'
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle mixed ASCII and Unicode characters', () => {
const original =
'{"nodes":[{"id":1,"type":"LoadImage","localized_name":"加载图像","label":"Load Image 图片"}]}'
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle emoji characters', () => {
const original = '{"title":"Test Node 🎨🖼️"}'
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle empty string', () => {
const original = ''
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
})
it('should handle complex node data with multiple Unicode fields', () => {
const original = JSON.stringify({
nodes: [
{
id: 1,
type: 'LoadImage',
localized_name: '图像',
inputs: [{ localized_name: '图片', name: 'image' }],
outputs: [{ localized_name: '输出', name: 'output' }]
}
],
groups: [{ title: '预处理组 🔧' }],
links: []
})
const encoded = encodeClipboardData(original)
const decoded = decodeClipboardData(encoded)
expect(decoded).toBe(original)
expect(JSON.parse(decoded)).toEqual(JSON.parse(original))
})
it('should produce valid base64 output', () => {
const original = '{"localized_name":"中文测试"}'
const encoded = encodeClipboardData(original)
// Base64 should only contain valid characters
expect(encoded).toMatch(/^[A-Za-z0-9+/=]+$/)
})
it('should fail with plain btoa for non-Latin1 characters', () => {
const original = '{"localized_name":"图像"}'
// This demonstrates why we need TextEncoder - plain btoa fails
expect(() => btoa(original)).toThrow()
})
})