@smallpen/local-package 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/fonts/SourceSansPro-Bold.ttf +0 -0
- package/assets/fonts/SourceSansPro-Regular.ttf +0 -0
- package/assets/fonts/SourceSansPro-Semibold.ttf +0 -0
- package/assets/fonts/WorkSans-Regular.ttf +0 -0
- package/package.json +23 -0
- package/src/draft-import.mjs +282 -0
- package/src/font-convert.mjs +132 -0
- package/src/index.mjs +34 -0
- package/src/initialize.mjs +533 -0
- package/src/kiwi-runtime.mjs +526 -0
- package/src/local-package-backend.mjs +460 -0
- package/src/local-package.mjs +842 -0
- package/src/local-workspace-session.mjs +486 -0
- package/src/media-inspect.mjs +184 -0
- package/src/remote-library.mjs +301 -0
- package/src/render.mjs +3605 -0
- package/src/webp-decode.mjs +73 -0
- package/src/workspace.mjs +591 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@smallpen/local-package",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": "./src/index.mjs",
|
|
9
|
+
"files": [
|
|
10
|
+
"assets",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@jsquash/webp": "1.5.0",
|
|
15
|
+
"@smallpen/core": "0.1.0-alpha.1",
|
|
16
|
+
"opentype.js": "2.0.0"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/SmallRaw/SmallPen.git",
|
|
21
|
+
"directory": "smallpen/packages/local-package"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
lstat,
|
|
4
|
+
mkdir,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
rename,
|
|
7
|
+
rm,
|
|
8
|
+
writeFile,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
11
|
+
import { inflateSync, zstdDecompressSync } from "node:zlib";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
canonicalJSON,
|
|
15
|
+
createFigmaDraftValues,
|
|
16
|
+
createFlatDraftValues,
|
|
17
|
+
fail,
|
|
18
|
+
listPackageEntries,
|
|
19
|
+
loadPackageFromValues,
|
|
20
|
+
sha256Hex,
|
|
21
|
+
SmallPenError,
|
|
22
|
+
} from "@smallpen/core";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
ByteBuffer,
|
|
26
|
+
compileSchema,
|
|
27
|
+
decodeBinarySchema,
|
|
28
|
+
} from "./kiwi-runtime.mjs";
|
|
29
|
+
|
|
30
|
+
const MAX_INPUT_BYTES = 50 * 1024 * 1024;
|
|
31
|
+
const MAX_DECOMPRESSED_BYTES = 128 * 1024 * 1024;
|
|
32
|
+
const ZSTD_MAGIC = [0x28, 0xb5, 0x2f, 0xfd];
|
|
33
|
+
|
|
34
|
+
function decodeBase64(value, label) {
|
|
35
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
36
|
+
fail("invalid_figma_clipboard", `${label} is missing`);
|
|
37
|
+
}
|
|
38
|
+
const bytes = Buffer.from(value.replace(/\s+/g, ""), "base64");
|
|
39
|
+
if (bytes.byteLength === 0 || bytes.byteLength > MAX_INPUT_BYTES) {
|
|
40
|
+
fail("invalid_figma_clipboard", `${label} is empty or too large`);
|
|
41
|
+
}
|
|
42
|
+
return new Uint8Array(bytes);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseFigKiwiChunks(binary) {
|
|
46
|
+
if (
|
|
47
|
+
binary.byteLength < 20 ||
|
|
48
|
+
new TextDecoder("ascii").decode(binary.slice(0, 8)) !== "fig-kiwi"
|
|
49
|
+
) {
|
|
50
|
+
fail("invalid_figma_clipboard", "Figma clipboard payload is not fig-kiwi");
|
|
51
|
+
}
|
|
52
|
+
const view = new DataView(binary.buffer, binary.byteOffset, binary.byteLength);
|
|
53
|
+
const chunks = [];
|
|
54
|
+
let offset = 12;
|
|
55
|
+
while (offset < binary.byteLength) {
|
|
56
|
+
if (offset + 4 > binary.byteLength) {
|
|
57
|
+
fail("invalid_figma_clipboard", "Figma clipboard chunk header is truncated");
|
|
58
|
+
}
|
|
59
|
+
const length = view.getUint32(offset, true);
|
|
60
|
+
offset += 4;
|
|
61
|
+
if (length <= 0 || offset + length > binary.byteLength) {
|
|
62
|
+
fail("invalid_figma_clipboard", "Figma clipboard chunk length is invalid");
|
|
63
|
+
}
|
|
64
|
+
chunks.push(binary.slice(offset, offset + length));
|
|
65
|
+
offset += length;
|
|
66
|
+
}
|
|
67
|
+
if (chunks.length < 2) {
|
|
68
|
+
fail("invalid_figma_clipboard", "Figma clipboard requires schema and data chunks");
|
|
69
|
+
}
|
|
70
|
+
return chunks;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isZstd(bytes) {
|
|
74
|
+
return ZSTD_MAGIC.every((byte, index) => bytes[index] === byte);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function decompress(bytes, label) {
|
|
78
|
+
try {
|
|
79
|
+
const result = isZstd(bytes)
|
|
80
|
+
? zstdDecompressSync(bytes, { maxOutputLength: MAX_DECOMPRESSED_BYTES })
|
|
81
|
+
: inflateSync(bytes, { maxOutputLength: MAX_DECOMPRESSED_BYTES });
|
|
82
|
+
return new Uint8Array(result);
|
|
83
|
+
} catch (error) {
|
|
84
|
+
fail(
|
|
85
|
+
"invalid_figma_clipboard",
|
|
86
|
+
`Unable to decompress Figma ${label}: ${error.message}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function decodeFigmaClipboard(html) {
|
|
92
|
+
if (typeof html !== "string" || Buffer.byteLength(html) > MAX_INPUT_BYTES) {
|
|
93
|
+
fail("invalid_figma_clipboard", "Figma clipboard HTML is invalid or too large");
|
|
94
|
+
}
|
|
95
|
+
const metaMatch = /\(figmeta\)(.*?)\(\/figmeta\)/s.exec(html);
|
|
96
|
+
const bufferMatch = /\(figma\)(.*?)\(\/figma\)/s.exec(html);
|
|
97
|
+
if (!metaMatch || !bufferMatch) {
|
|
98
|
+
fail(
|
|
99
|
+
"invalid_figma_clipboard",
|
|
100
|
+
"Input does not contain Figma structured clipboard markers",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
let meta;
|
|
104
|
+
try {
|
|
105
|
+
meta = JSON.parse(Buffer.from(metaMatch[1], "base64").toString("utf8"));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
fail("invalid_figma_clipboard", `Figma clipboard metadata is invalid: ${error.message}`);
|
|
108
|
+
}
|
|
109
|
+
const chunks = parseFigKiwiChunks(decodeBase64(bufferMatch[1], "Figma clipboard payload"));
|
|
110
|
+
try {
|
|
111
|
+
const schema = decodeBinarySchema(new ByteBuffer(decompress(chunks[0], "schema")));
|
|
112
|
+
const compiled = compileSchema(schema);
|
|
113
|
+
if (typeof compiled.decodeMessage !== "function") {
|
|
114
|
+
fail("invalid_figma_clipboard", "Figma clipboard schema has no Message decoder");
|
|
115
|
+
}
|
|
116
|
+
const message = compiled.decodeMessage(decompress(chunks[1], "data"));
|
|
117
|
+
const nodeChanges = message.nodeChanges ?? [];
|
|
118
|
+
if (!Array.isArray(nodeChanges)) {
|
|
119
|
+
fail("invalid_figma_clipboard", "Figma clipboard nodeChanges are invalid");
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
blobCount: Array.isArray(message.blobs) ? message.blobs.length : 0,
|
|
123
|
+
meta,
|
|
124
|
+
nodeChanges,
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof SmallPenError) throw error;
|
|
128
|
+
fail("invalid_figma_clipboard", `Unable to decode Figma clipboard: ${error.message}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function inspectPng(bytes) {
|
|
133
|
+
const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
134
|
+
if (
|
|
135
|
+
bytes.byteLength < 24 ||
|
|
136
|
+
signature.some((byte, index) => bytes[index] !== byte) ||
|
|
137
|
+
new TextDecoder("ascii").decode(bytes.slice(12, 16)) !== "IHDR"
|
|
138
|
+
) {
|
|
139
|
+
fail("invalid_draft_media", "Draft PNG has an invalid signature or IHDR");
|
|
140
|
+
}
|
|
141
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
142
|
+
return { height: view.getUint32(20), width: view.getUint32(16) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function inspectSvg(bytes) {
|
|
146
|
+
let source;
|
|
147
|
+
try {
|
|
148
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
fail("invalid_draft_media", `Draft SVG is not UTF-8: ${error.message}`);
|
|
151
|
+
}
|
|
152
|
+
const root = /<svg\b([^>]*)>/i.exec(source)?.[1];
|
|
153
|
+
if (root === undefined) fail("invalid_draft_media", "Draft SVG has no svg root");
|
|
154
|
+
const attribute = (name) =>
|
|
155
|
+
new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i").exec(root)?.[1];
|
|
156
|
+
const length = (value) => {
|
|
157
|
+
const match = /^\s*([0-9]+(?:\.[0-9]+)?)(?:px)?\s*$/i.exec(value ?? "");
|
|
158
|
+
return match ? Math.round(Number(match[1])) : undefined;
|
|
159
|
+
};
|
|
160
|
+
let width = length(attribute("width"));
|
|
161
|
+
let height = length(attribute("height"));
|
|
162
|
+
const viewBox = attribute("viewBox")?.trim().split(/[\s,]+/).map(Number);
|
|
163
|
+
if ((!width || !height) && viewBox?.length === 4 && viewBox.every(Number.isFinite)) {
|
|
164
|
+
width ??= Math.round(viewBox[2]);
|
|
165
|
+
height ??= Math.round(viewBox[3]);
|
|
166
|
+
}
|
|
167
|
+
if (!Number.isSafeInteger(width) || width <= 0 || !Number.isSafeInteger(height) || height <= 0) {
|
|
168
|
+
fail("invalid_draft_media", "Draft SVG dimensions are invalid");
|
|
169
|
+
}
|
|
170
|
+
return { height, width };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function pathExists(path) {
|
|
174
|
+
try {
|
|
175
|
+
await lstat(path);
|
|
176
|
+
return true;
|
|
177
|
+
} catch (error) {
|
|
178
|
+
if (error?.code === "ENOENT") return false;
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function writeDraftPackage(outputValue, values) {
|
|
184
|
+
const output = resolve(outputValue);
|
|
185
|
+
if (!output.endsWith(".smallpen")) {
|
|
186
|
+
fail("invalid_draft_path", "Draft output must end in .smallpen", { output });
|
|
187
|
+
}
|
|
188
|
+
if (await pathExists(output)) {
|
|
189
|
+
fail("draft_output_exists", "Draft output already exists and will not be overwritten", {
|
|
190
|
+
output,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
await mkdir(dirname(output), { recursive: true });
|
|
194
|
+
const transaction = await mkdtemp(join(dirname(output), ".smallpen-draft-"));
|
|
195
|
+
const candidate = join(transaction, `${basename(output)}-${randomUUID()}.smallpen`);
|
|
196
|
+
try {
|
|
197
|
+
const loaded = await loadPackageFromValues(candidate, values);
|
|
198
|
+
await mkdir(candidate);
|
|
199
|
+
const { entries } = listPackageEntries(loaded.manifest);
|
|
200
|
+
for (const entry of ["manifest.json", ...entries]) {
|
|
201
|
+
const path = join(candidate, entry);
|
|
202
|
+
await mkdir(dirname(path), { recursive: true });
|
|
203
|
+
await writeFile(
|
|
204
|
+
path,
|
|
205
|
+
canonicalJSON(entry === "manifest.json" ? loaded.manifest : loaded.entries[entry]),
|
|
206
|
+
"utf8",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
for (const [entry, bytes] of loaded.blobs) {
|
|
210
|
+
const path = join(candidate, entry);
|
|
211
|
+
await mkdir(dirname(path), { recursive: true });
|
|
212
|
+
await writeFile(path, bytes);
|
|
213
|
+
}
|
|
214
|
+
if (await pathExists(output)) {
|
|
215
|
+
fail("draft_output_exists", "Draft output appeared during import and was not overwritten", {
|
|
216
|
+
output,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
await rename(candidate, output);
|
|
220
|
+
await rm(transaction, { force: true, recursive: true });
|
|
221
|
+
return { ...loaded, locator: output };
|
|
222
|
+
} catch (error) {
|
|
223
|
+
await rm(transaction, { force: true, recursive: true });
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function importDraft({
|
|
229
|
+
bytes,
|
|
230
|
+
html,
|
|
231
|
+
importedAt = new Date().toISOString(),
|
|
232
|
+
kind,
|
|
233
|
+
output,
|
|
234
|
+
packageId = "pkg_figma_draft",
|
|
235
|
+
}) {
|
|
236
|
+
if (!new Set(["figma", "png", "svg"]).has(kind)) {
|
|
237
|
+
fail("invalid_draft_kind", "Draft import kind must be figma, svg, or png");
|
|
238
|
+
}
|
|
239
|
+
let model;
|
|
240
|
+
if (kind === "figma") {
|
|
241
|
+
const input = new TextEncoder().encode(html ?? "");
|
|
242
|
+
const decoded = await decodeFigmaClipboard(html);
|
|
243
|
+
model = createFigmaDraftValues({
|
|
244
|
+
importedAt,
|
|
245
|
+
inputHash: await sha256Hex(input),
|
|
246
|
+
meta: decoded.meta,
|
|
247
|
+
nodeChanges: decoded.nodeChanges,
|
|
248
|
+
packageId,
|
|
249
|
+
});
|
|
250
|
+
if (decoded.blobCount > 0) {
|
|
251
|
+
model.losses.push({
|
|
252
|
+
code: "figma_binary_blobs_not_embedded",
|
|
253
|
+
message: `${decoded.blobCount} Figma binary image/font blobs remain source-only in this Draft`,
|
|
254
|
+
severity: "warning",
|
|
255
|
+
});
|
|
256
|
+
model.values.get("manifest.json").draft.losses = model.losses;
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
if (!(bytes instanceof Uint8Array) || bytes.byteLength === 0 || bytes.byteLength > MAX_INPUT_BYTES) {
|
|
260
|
+
fail("invalid_draft_media", "Draft fallback bytes are empty or too large");
|
|
261
|
+
}
|
|
262
|
+
const dimensions = kind === "png" ? inspectPng(bytes) : inspectSvg(bytes);
|
|
263
|
+
const inputHash = await sha256Hex(bytes);
|
|
264
|
+
model = createFlatDraftValues({
|
|
265
|
+
bytes,
|
|
266
|
+
...dimensions,
|
|
267
|
+
importedAt,
|
|
268
|
+
inputHash,
|
|
269
|
+
mimeType: kind === "png" ? "image/png" : "image/svg+xml",
|
|
270
|
+
packageId,
|
|
271
|
+
sourceKind: kind,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const written = await writeDraftPackage(output, model.values);
|
|
275
|
+
return {
|
|
276
|
+
losses: model.losses,
|
|
277
|
+
output: written.locator,
|
|
278
|
+
packageId: written.manifest.packageId,
|
|
279
|
+
provenance: model.provenance,
|
|
280
|
+
revision: written.revision,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { deflateSync } from "node:zlib";
|
|
2
|
+
|
|
3
|
+
import { SmallPenError } from "@smallpen/core";
|
|
4
|
+
|
|
5
|
+
// SFNT -> WOFF repackaging shared by the CLI and the Background font service.
|
|
6
|
+
|
|
7
|
+
const FONT_SIGNATURES = {
|
|
8
|
+
"font/otf": new Uint8Array([0x4f, 0x54, 0x54, 0x4f]),
|
|
9
|
+
"font/ttf": new Uint8Array([0x00, 0x01, 0x00, 0x00]),
|
|
10
|
+
"font/woff": new Uint8Array([0x77, 0x4f, 0x46, 0x46]),
|
|
11
|
+
"font/woff2": new Uint8Array([0x77, 0x4f, 0x46, 0x32]),
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function failFont(code, message, details) {
|
|
15
|
+
throw new SmallPenError(code, message, details);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function matches(bytes, signature) {
|
|
19
|
+
return signature.every((value, index) => bytes[index] === value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function inspectFont(bytes, mimeType) {
|
|
23
|
+
const signature = FONT_SIGNATURES[mimeType];
|
|
24
|
+
if (!signature || !matches(bytes, signature)) {
|
|
25
|
+
failFont(
|
|
26
|
+
"invalid_font_blob",
|
|
27
|
+
`Font bytes do not match ${mimeType || "an allowed Font type"}`,
|
|
28
|
+
{ mimeType },
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
return mimeType;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function align4(value) {
|
|
35
|
+
return (value + 3) & ~3;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tableRecords(bytes) {
|
|
39
|
+
if (bytes.byteLength < 12) {
|
|
40
|
+
failFont("invalid_font_blob", "SFNT Font header is truncated");
|
|
41
|
+
}
|
|
42
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
43
|
+
const numTables = view.getUint16(4, false);
|
|
44
|
+
const directoryEnd = 12 + numTables * 16;
|
|
45
|
+
if (numTables === 0 || numTables > 4096 || directoryEnd > bytes.byteLength) {
|
|
46
|
+
failFont("invalid_font_blob", "SFNT Font table directory is invalid");
|
|
47
|
+
}
|
|
48
|
+
const records = [];
|
|
49
|
+
for (let index = 0; index < numTables; index += 1) {
|
|
50
|
+
const position = 12 + index * 16;
|
|
51
|
+
const offset = view.getUint32(position + 8, false);
|
|
52
|
+
const length = view.getUint32(position + 12, false);
|
|
53
|
+
if (offset > bytes.byteLength || length > bytes.byteLength - offset) {
|
|
54
|
+
failFont("invalid_font_blob", "SFNT Font table extends beyond the file");
|
|
55
|
+
}
|
|
56
|
+
records.push({
|
|
57
|
+
checksum: view.getUint32(position + 4, false),
|
|
58
|
+
data: bytes.subarray(offset, offset + length),
|
|
59
|
+
length,
|
|
60
|
+
tag: view.getUint32(position, false),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return records.sort((left, right) => left.tag - right.tag);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function sfntToWoff(bytes, mimeType) {
|
|
67
|
+
if (mimeType !== "font/ttf" && mimeType !== "font/otf") {
|
|
68
|
+
failFont("unsupported_font_conversion", `Cannot convert ${mimeType} to WOFF`);
|
|
69
|
+
}
|
|
70
|
+
inspectFont(bytes, mimeType);
|
|
71
|
+
const source = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
72
|
+
const records = tableRecords(bytes).map((record) => {
|
|
73
|
+
const compressed = new Uint8Array(deflateSync(record.data));
|
|
74
|
+
return {
|
|
75
|
+
...record,
|
|
76
|
+
stored: compressed.byteLength < record.length ? compressed : record.data,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const directoryLength = 44 + records.length * 20;
|
|
80
|
+
let outputLength = directoryLength;
|
|
81
|
+
for (const record of records) outputLength += align4(record.stored.byteLength);
|
|
82
|
+
const output = new Uint8Array(outputLength);
|
|
83
|
+
const view = new DataView(output.buffer);
|
|
84
|
+
view.setUint32(0, 0x774f4646, false);
|
|
85
|
+
view.setUint32(4, source.getUint32(0, false), false);
|
|
86
|
+
view.setUint32(8, outputLength, false);
|
|
87
|
+
view.setUint16(12, records.length, false);
|
|
88
|
+
view.setUint16(14, 0, false);
|
|
89
|
+
view.setUint32(
|
|
90
|
+
16,
|
|
91
|
+
12 + records.length * 16 +
|
|
92
|
+
records.reduce((total, record) => total + align4(record.length), 0),
|
|
93
|
+
false,
|
|
94
|
+
);
|
|
95
|
+
view.setUint16(20, 1, false);
|
|
96
|
+
view.setUint16(22, 0, false);
|
|
97
|
+
let dataOffset = directoryLength;
|
|
98
|
+
for (const [index, record] of records.entries()) {
|
|
99
|
+
const position = 44 + index * 20;
|
|
100
|
+
view.setUint32(position, record.tag, false);
|
|
101
|
+
view.setUint32(position + 4, dataOffset, false);
|
|
102
|
+
view.setUint32(position + 8, record.stored.byteLength, false);
|
|
103
|
+
view.setUint32(position + 12, record.length, false);
|
|
104
|
+
view.setUint32(position + 16, record.checksum, false);
|
|
105
|
+
output.set(record.stored, dataOffset);
|
|
106
|
+
dataOffset += align4(record.stored.byteLength);
|
|
107
|
+
}
|
|
108
|
+
return output;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function prepareFontFiles(input) {
|
|
112
|
+
const files = {};
|
|
113
|
+
for (const [mimeType, bytes] of Object.entries(input)) {
|
|
114
|
+
inspectFont(bytes, mimeType);
|
|
115
|
+
const format = mimeType.slice("font/".length);
|
|
116
|
+
files[format] = { bytes, mimeType };
|
|
117
|
+
}
|
|
118
|
+
if (!files.woff) {
|
|
119
|
+
const source = files.ttf ?? files.otf;
|
|
120
|
+
if (!source) {
|
|
121
|
+
failFont(
|
|
122
|
+
"unsupported_font_conversion",
|
|
123
|
+
"A WOFF2-only upload cannot be converted without a WOFF2 decoder",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
files.woff = {
|
|
127
|
+
bytes: sfntToWoff(source.bytes, source.mimeType),
|
|
128
|
+
mimeType: "font/woff",
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return files;
|
|
132
|
+
}
|
package/src/index.mjs
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export { LocalPackageBackend } from "./local-package-backend.mjs";
|
|
2
|
+
export {
|
|
3
|
+
LocalWorkspaceSession,
|
|
4
|
+
reconcilePackageViewState,
|
|
5
|
+
} from "./local-workspace-session.mjs";
|
|
6
|
+
export { openWorkspace, resolveWorkspace } from "./workspace.mjs";
|
|
7
|
+
export {
|
|
8
|
+
defaultLibraryCacheRoot,
|
|
9
|
+
openRemoteLibrary,
|
|
10
|
+
} from "./remote-library.mjs";
|
|
11
|
+
export { createEvidence, renderProjection } from "./render.mjs";
|
|
12
|
+
export {
|
|
13
|
+
decodeFigmaClipboard,
|
|
14
|
+
importDraft,
|
|
15
|
+
writeDraftPackage,
|
|
16
|
+
} from "./draft-import.mjs";
|
|
17
|
+
export { createBlankPackage, initializeWorkspace } from "./initialize.mjs";
|
|
18
|
+
export {
|
|
19
|
+
applyOperationBatch,
|
|
20
|
+
deleteFontFamily,
|
|
21
|
+
deleteFontVariant,
|
|
22
|
+
importFontVariant,
|
|
23
|
+
importMedia,
|
|
24
|
+
openPackage,
|
|
25
|
+
removeMedia,
|
|
26
|
+
replayRecordedBatch,
|
|
27
|
+
updateFontFamily,
|
|
28
|
+
} from "./local-package.mjs";
|
|
29
|
+
export { detectMediaType, inspectMedia } from "./media-inspect.mjs";
|
|
30
|
+
export {
|
|
31
|
+
inspectFont,
|
|
32
|
+
prepareFontFiles,
|
|
33
|
+
sfntToWoff,
|
|
34
|
+
} from "./font-convert.mjs";
|