Merge pull request #256 from guimaurer/fix/image-fill-hash-encoding
fix: image fills lost after save . hex hash corrupted in Kiwi byte[] encoding
This commit is contained in:
commit
123a5ceddb
21
packages/core/src/bytes/hex.ts
Normal file
21
packages/core/src/bytes/hex.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
export function hexToBytes(hex: string): Uint8Array {
|
||||
if (hex.length % 2 !== 0) {
|
||||
throw new Error('Hex string must contain an even number of characters')
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const byte = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16)
|
||||
if (Number.isNaN(byte)) throw new Error('Hex string contains invalid characters')
|
||||
bytes[i] = byte
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
export function bytesToHex(bytes: Uint8Array): string {
|
||||
let hex = ''
|
||||
for (const byte of bytes) {
|
||||
hex += byte.toString(16).padStart(2, '0')
|
||||
}
|
||||
return hex
|
||||
}
|
||||
|
|
@ -318,22 +318,11 @@ function compressViaWorker(
|
|||
worker.terminate()
|
||||
}
|
||||
|
||||
const imgCopies = imageEntries.map((e) => ({
|
||||
name: e.name,
|
||||
data: new Uint8Array(e.data)
|
||||
}))
|
||||
|
||||
const transferables = [
|
||||
schemaDeflated.buffer,
|
||||
kiwiData.buffer,
|
||||
thumbnailPng.buffer,
|
||||
...imgCopies.map((e) => e.data.buffer)
|
||||
]
|
||||
|
||||
worker.postMessage(
|
||||
{ schemaDeflated, kiwiData, thumbnailPng, metaJson, images: imgCopies },
|
||||
transferables
|
||||
)
|
||||
// Do NOT use transferables here. toUint8Array() in ByteBuffer returns a view of the
|
||||
// internal buffer, so transferring kiwiData.buffer or schemaDeflated.buffer detaches
|
||||
// buffers that may be shared with other views, causing "already detached" errors on
|
||||
// subsequent saves. Structured clone (the default) copies the data safely.
|
||||
worker.postMessage({ schemaDeflated, kiwiData, thumbnailPng, metaJson, images: imageEntries })
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ export interface Paint {
|
|||
blendMode?: string
|
||||
stops?: { color: Color; position: number }[]
|
||||
transform?: Matrix
|
||||
image?: { hash: string }
|
||||
image?: { hash: string | Uint8Array }
|
||||
imageScaleMode?: string
|
||||
colorVariableBinding?: VariableBinding
|
||||
colorVar?: {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { hexToBytes } from '#core/bytes/hex'
|
||||
import type { GUID } from '#core/types'
|
||||
|
||||
import type { NodeChange, Paint } from './codec'
|
||||
|
|
@ -87,7 +88,7 @@ export function encodeNodeChangeWithVariables(
|
|||
hex = injectVariableBinding(hex, '2701', strokeBinding)
|
||||
}
|
||||
|
||||
return new Uint8Array(hex.match(/.{2}/g)?.map((b) => parseInt(b, 16)) ?? [])
|
||||
return hexToBytes(hex)
|
||||
}
|
||||
|
||||
function injectVariableBinding(hex: string, marker: string, binding: { variableID: GUID }): string {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { hexToBytes } from '#core/bytes/hex'
|
||||
import { buildDerivedTextData as buildSharedDerivedTextData } from '#core/text/derived-text/data'
|
||||
import { normalizeFontFamily, weightToFigmaStyle, weightToStyle } from '#core/text/fonts'
|
||||
import { encodePathCommandsBlob } from '#core/kiwi/node-change/path-commands'
|
||||
|
|
@ -205,7 +206,7 @@ function fillToKiwiPaint(f: SceneNode['fills'][number]): Paint {
|
|||
paint.stops = f.gradientStops.map((s) => ({ color: safeColor(s.color), position: s.position }))
|
||||
}
|
||||
if (f.gradientTransform) paint.transform = f.gradientTransform
|
||||
if (f.imageHash) paint.image = { hash: f.imageHash }
|
||||
if (f.imageHash) paint.image = { hash: hexToBytes(f.imageHash) }
|
||||
if (f.imageScaleMode) paint.imageScaleMode = f.imageScaleMode
|
||||
if (f.imageTransform) paint.transform = f.imageTransform
|
||||
return paint
|
||||
|
|
|
|||
|
|
@ -108,5 +108,16 @@ describe('fig export/import with images', () => {
|
|||
expect(new Uint8Array(expectDefined(restored.images.get(hash2), 'restored image 2'))).toEqual(
|
||||
bytes2
|
||||
)
|
||||
|
||||
const restoredImg1 = expectDefined(
|
||||
[...restored.getAllNodes()].find((node) => node.name === 'Img1'),
|
||||
'restored image node 1'
|
||||
)
|
||||
const restoredImg2 = expectDefined(
|
||||
[...restored.getAllNodes()].find((node) => node.name === 'Img2'),
|
||||
'restored image node 2'
|
||||
)
|
||||
expect(restoredImg1.fills[0].imageHash).toBe(hash1)
|
||||
expect(restoredImg2.fills[0].imageHash).toBe(hash2)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue