76 lines
5.5 KiB
JavaScript
76 lines
5.5 KiB
JavaScript
// Génère des .fig de test pour valider ce que Figma accepte à l'import
|
|
import fs from 'node:fs';
|
|
import crypto from 'node:crypto';
|
|
import JSZip from 'jszip';
|
|
import { readCanvas, writeCanvas } from '../src/figfile.js';
|
|
|
|
const [,, refPath, outDir] = process.argv;
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
const refZip = await JSZip.loadAsync(fs.readFileSync(refPath));
|
|
const ref = readCanvas(await refZip.file('canvas.fig').async('uint8array'));
|
|
const meta = await refZip.file('meta.json').async('string');
|
|
const thumb = await refZip.file('thumbnail.png').async('uint8array');
|
|
|
|
async function pack(name, canvas, images = {}) {
|
|
const zip = new JSZip();
|
|
zip.file('canvas.fig', writeCanvas(canvas));
|
|
zip.file('thumbnail.png', thumb);
|
|
const m = JSON.parse(meta); m.file_name = name;
|
|
zip.file('meta.json', JSON.stringify(m));
|
|
zip.folder('images');
|
|
for (const [h, b] of Object.entries(images)) zip.file(`images/${h}`, b);
|
|
fs.writeFileSync(`${outDir}/${name}.fig`, await zip.generateAsync({ type: 'uint8array', compression: 'STORE' }));
|
|
}
|
|
|
|
// Test 1 : aller-retour décodage/réencodage du fichier de référence
|
|
const refImages = {};
|
|
for (const f of Object.keys(refZip.files).filter(f => f.startsWith('images/') && !refZip.files[f].dir))
|
|
refImages[f.slice(7)] = await refZip.file(f).async('uint8array');
|
|
await pack('test1-roundtrip', ref, refImages);
|
|
|
|
// Test 2 : document minimal construit de zéro, sans géométrie ni layout texte précalculés
|
|
const id = (localID) => ({ sessionID: 1, localID });
|
|
const T = (x, y) => ({ m00: 1, m01: 0, m02: x, m10: 0, m11: 1, m12: y });
|
|
const solid = (r, g, b, a = 1) => ({ type: 'SOLID', color: { r, g, b, a }, opacity: 1, visible: true, blendMode: 'NORMAL' });
|
|
const base = { phase: 'CREATED', visible: true, opacity: 1, strokeWeight: 1, strokeAlign: 'INSIDE', strokeJoin: 'MITER' };
|
|
const imgBytes = fs.readFileSync(process.argv[4]);
|
|
const imgHash = crypto.createHash('sha1').update(imgBytes).digest();
|
|
const imgHex = imgHash.toString('hex');
|
|
|
|
// blob de géométrie : 1=moveTo, 2=lineTo, 4=cubicTo, 0=close ; float32 LE
|
|
function geomBlob(cmds) {
|
|
const parts = [];
|
|
for (const [op, ...pts] of cmds) {
|
|
const b = Buffer.alloc(1 + pts.length * 4); b[0] = op;
|
|
pts.forEach((p, i) => b.writeFloatLE(p, 1 + i * 4)); parts.push(b);
|
|
}
|
|
return { bytes: new Uint8Array(Buffer.concat(parts)) };
|
|
}
|
|
|
|
function minimal({ withGeometry }) {
|
|
const blobs = [];
|
|
const tri = geomBlob([[1, 50, 0], [2, 100, 100], [2, 0, 100], [2, 50, 0], [0]]);
|
|
blobs.push(tri);
|
|
const nodes = [
|
|
{ ...base, guid: { sessionID: 0, localID: 0 }, type: 'DOCUMENT', name: 'Document', transform: T(0, 0), documentColorProfile: 'SRGB' },
|
|
{ ...base, guid: { sessionID: 0, localID: 1 }, parentIndex: { guid: { sessionID: 0, localID: 0 }, position: '!' }, type: 'CANVAS', name: 'Page 1', transform: T(0, 0), backgroundOpacity: 1, backgroundEnabled: true, backgroundColor: { r: 0.9, g: 0.9, b: 0.9, a: 1 } },
|
|
{ ...base, guid: id(1), parentIndex: { guid: { sessionID: 0, localID: 1 }, position: '!' }, type: 'FRAME', name: 'Artboard test', size: { x: 800, y: 600 }, transform: T(0, 0), fillPaints: [solid(1, 1, 1)], resizeToFit: false, frameMaskDisabled: false },
|
|
{ ...base, guid: id(2), parentIndex: { guid: id(1), position: '!' }, type: 'ROUNDED_RECTANGLE', name: 'Rect rouge arrondi', size: { x: 200, y: 120 }, transform: T(40, 40), fillPaints: [solid(0.9, 0.1, 0.2)], cornerRadius: 16,
|
|
effects: [{ type: 'DROP_SHADOW', color: { r: 0, g: 0, b: 0, a: 0.4 }, offset: { x: 0, y: 8 }, radius: 16, spread: 0, visible: true, blendMode: 'NORMAL', showShadowBehindNode: false }] },
|
|
{ ...base, guid: id(3), parentIndex: { guid: id(1), position: '"' }, type: 'ELLIPSE', name: 'Cercle dégradé', size: { x: 150, y: 150 }, transform: T(300, 40),
|
|
fillPaints: [{ type: 'GRADIENT_LINEAR', opacity: 1, visible: true, blendMode: 'NORMAL', transform: T(0, 0), stops: [{ color: { r: 0, g: 0.4, b: 1, a: 1 }, position: 0 }, { color: { r: 1, g: 0.8, b: 0, a: 1 }, position: 1 }] }] },
|
|
{ ...base, guid: id(4), parentIndex: { guid: id(1), position: '#' }, type: 'ROUNDED_RECTANGLE', name: 'Image', size: { x: 300, y: 200 }, transform: T(40, 250),
|
|
fillPaints: [{ type: 'IMAGE', opacity: 1, visible: true, blendMode: 'NORMAL', transform: T(0, 0), image: { hash: new Uint8Array(imgHash), name: 'image' }, imageScaleMode: 'FILL', imageShouldColorManage: false, rotation: 0, scale: 1 }] },
|
|
{ ...base, guid: id(5), parentIndex: { guid: id(1), position: '$' }, type: 'TEXT', name: 'Texte', size: { x: 400, y: 40 }, transform: T(380, 250), strokeAlign: 'OUTSIDE',
|
|
fillPaints: [solid(0.1, 0.1, 0.1)], fontSize: 32, fontName: { family: 'Roboto', style: 'Bold', postscript: 'Roboto-Bold' },
|
|
lineHeight: { value: 100, units: 'PERCENT' }, letterSpacing: { value: 0, units: 'PERCENT' }, textAlignHorizontal: 'LEFT', textAlignVertical: 'TOP', textAutoResize: 'WIDTH_AND_HEIGHT',
|
|
textData: { characters: 'Bonjour de XD → Figma' } },
|
|
{ ...base, guid: id(6), parentIndex: { guid: id(1), position: '%' }, type: 'VECTOR', name: 'Triangle', size: { x: 100, y: 100 }, transform: T(500, 40), fillPaints: [solid(0.2, 0.7, 0.3)],
|
|
...(withGeometry ? { fillGeometry: [{ windingRule: 'NONZERO', commandsBlob: 0, styleID: 0 }] } : {}) },
|
|
];
|
|
return { ...ref, message: { type: 'NODE_CHANGES', sessionID: 0, ackID: 0, nodeChanges: nodes, blobs } };
|
|
}
|
|
await pack('test2-minimal', minimal({ withGeometry: true }), { [imgHex]: imgBytes });
|
|
await pack('test3-sans-geometrie', minimal({ withGeometry: false }), { [imgHex]: imgBytes });
|
|
console.log('ok');
|