@effing/canvas 0.24.5 → 0.24.7
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/dist/index.js +144 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -260,29 +260,87 @@ function resolveTransformUnits(transform, viewportWidth, viewportHeight, fontSiz
|
|
|
260
260
|
import { GlobalFonts } from "@napi-rs/canvas";
|
|
261
261
|
|
|
262
262
|
// src/jsx/font-metrics.ts
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
import { inflateSync } from "zlib";
|
|
264
|
+
var WOFF_SIGNATURE = 2001684038;
|
|
265
|
+
function readWoffTable(view, entry) {
|
|
266
|
+
const compressed = entry.compLength < entry.origLength;
|
|
267
|
+
if (compressed) {
|
|
268
|
+
try {
|
|
269
|
+
const compData = new Uint8Array(
|
|
270
|
+
view.buffer,
|
|
271
|
+
view.byteOffset + entry.offset,
|
|
272
|
+
entry.compLength
|
|
273
|
+
);
|
|
274
|
+
const decompressed = inflateSync(compData);
|
|
275
|
+
return new DataView(
|
|
276
|
+
decompressed.buffer,
|
|
277
|
+
decompressed.byteOffset,
|
|
278
|
+
decompressed.byteLength
|
|
279
|
+
);
|
|
280
|
+
} catch {
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return new DataView(
|
|
285
|
+
view.buffer,
|
|
286
|
+
view.byteOffset + entry.offset,
|
|
287
|
+
entry.origLength
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
function parseWoff(view) {
|
|
291
|
+
if (view.byteLength < 44) return null;
|
|
292
|
+
const numTables = view.getUint16(12);
|
|
293
|
+
if (view.byteLength < 44 + numTables * 20) return null;
|
|
294
|
+
let headEntry = null;
|
|
295
|
+
let hheaEntry = null;
|
|
296
|
+
for (let i = 0; i < numTables; i++) {
|
|
297
|
+
const r = 44 + i * 20;
|
|
298
|
+
const tag = String.fromCharCode(view.getUint8(r)) + String.fromCharCode(view.getUint8(r + 1)) + String.fromCharCode(view.getUint8(r + 2)) + String.fromCharCode(view.getUint8(r + 3));
|
|
299
|
+
const offset = view.getUint32(r + 4);
|
|
300
|
+
const compLength = view.getUint32(r + 8);
|
|
301
|
+
const origLength = view.getUint32(r + 12);
|
|
302
|
+
if (tag === "head") headEntry = { offset, compLength, origLength };
|
|
303
|
+
else if (tag === "hhea") hheaEntry = { offset, compLength, origLength };
|
|
304
|
+
}
|
|
305
|
+
if (!headEntry || !hheaEntry) return null;
|
|
306
|
+
const headView = readWoffTable(view, headEntry);
|
|
307
|
+
const hheaView = readWoffTable(view, hheaEntry);
|
|
308
|
+
if (!headView || !hheaView) return null;
|
|
309
|
+
if (headView.byteLength < 20 || hheaView.byteLength < 8) return null;
|
|
310
|
+
const unitsPerEm = headView.getUint16(18);
|
|
311
|
+
const ascender = hheaView.getInt16(4);
|
|
312
|
+
const descender = hheaView.getInt16(6);
|
|
313
|
+
return { unitsPerEm, ascender, descender };
|
|
314
|
+
}
|
|
315
|
+
function parseSfnt(view) {
|
|
267
316
|
const numTables = view.getUint16(4);
|
|
268
|
-
if (
|
|
317
|
+
if (view.byteLength < 12 + numTables * 16) return null;
|
|
269
318
|
let headOffset = -1;
|
|
270
|
-
let
|
|
319
|
+
let hheaOffset = -1;
|
|
271
320
|
for (let i = 0; i < numTables; i++) {
|
|
272
321
|
const recordOffset = 12 + i * 16;
|
|
273
322
|
const tag = String.fromCharCode(view.getUint8(recordOffset)) + String.fromCharCode(view.getUint8(recordOffset + 1)) + String.fromCharCode(view.getUint8(recordOffset + 2)) + String.fromCharCode(view.getUint8(recordOffset + 3));
|
|
274
323
|
const offset = view.getUint32(recordOffset + 8);
|
|
275
324
|
if (tag === "head") headOffset = offset;
|
|
276
|
-
else if (tag === "
|
|
325
|
+
else if (tag === "hhea") hheaOffset = offset;
|
|
277
326
|
}
|
|
278
|
-
if (headOffset < 0 ||
|
|
279
|
-
if (
|
|
327
|
+
if (headOffset < 0 || hheaOffset < 0) return null;
|
|
328
|
+
if (view.byteLength < headOffset + 20) return null;
|
|
280
329
|
const unitsPerEm = view.getUint16(headOffset + 18);
|
|
281
|
-
if (
|
|
282
|
-
const
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
330
|
+
if (view.byteLength < hheaOffset + 8) return null;
|
|
331
|
+
const ascender = view.getInt16(hheaOffset + 4);
|
|
332
|
+
const descender = view.getInt16(hheaOffset + 6);
|
|
333
|
+
return { unitsPerEm, ascender, descender };
|
|
334
|
+
}
|
|
335
|
+
function parseFontMetrics(data) {
|
|
336
|
+
const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
337
|
+
const view = new DataView(buf);
|
|
338
|
+
if (buf.byteLength < 12) return null;
|
|
339
|
+
const signature = view.getUint32(0);
|
|
340
|
+
if (signature === WOFF_SIGNATURE) {
|
|
341
|
+
return parseWoff(view);
|
|
342
|
+
}
|
|
343
|
+
return parseSfnt(view);
|
|
286
344
|
}
|
|
287
345
|
|
|
288
346
|
// src/jsx/font.ts
|
|
@@ -300,10 +358,13 @@ function registerFont(font) {
|
|
|
300
358
|
registeredFonts.add(key);
|
|
301
359
|
}
|
|
302
360
|
function getFontMetrics(family, weight, style) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (
|
|
361
|
+
const families = family.split(",").map((f) => f.trim().replace(/^['"]|['"]$/g, ""));
|
|
362
|
+
for (const name of families) {
|
|
363
|
+
const exact = metricsCache.get(`${name}:${weight}:${style}`);
|
|
364
|
+
if (exact) return exact;
|
|
365
|
+
for (const [key, metrics] of metricsCache) {
|
|
366
|
+
if (key.startsWith(`${name}:`)) return metrics;
|
|
367
|
+
}
|
|
307
368
|
}
|
|
308
369
|
return null;
|
|
309
370
|
}
|
|
@@ -413,8 +474,8 @@ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHei
|
|
|
413
474
|
let fontAscent;
|
|
414
475
|
let fontDescent;
|
|
415
476
|
if (fontMetrics) {
|
|
416
|
-
fontAscent = fontMetrics.
|
|
417
|
-
fontDescent = -fontMetrics.
|
|
477
|
+
fontAscent = fontMetrics.ascender / fontMetrics.unitsPerEm * fontSize;
|
|
478
|
+
fontDescent = -fontMetrics.descender / fontMetrics.unitsPerEm * fontSize;
|
|
418
479
|
} else {
|
|
419
480
|
fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
|
|
420
481
|
fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
|
|
@@ -727,8 +788,8 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
|
|
|
727
788
|
function resolveLineHeight(lineHeight, fontSize, canvasMetrics, fontMetrics) {
|
|
728
789
|
if (lineHeight === void 0 || lineHeight === "normal") {
|
|
729
790
|
if (fontMetrics) {
|
|
730
|
-
const {
|
|
731
|
-
return (
|
|
791
|
+
const { ascender, descender, unitsPerEm } = fontMetrics;
|
|
792
|
+
return (ascender - descender) / unitsPerEm * fontSize;
|
|
732
793
|
}
|
|
733
794
|
return canvasMetrics ? canvasMetrics.ascent + canvasMetrics.descent : fontSize * 1.2;
|
|
734
795
|
}
|
|
@@ -2325,6 +2386,7 @@ function drawSvgContainer(ctx, node, x, y, width, height) {
|
|
|
2325
2386
|
|
|
2326
2387
|
// src/jsx/draw/text.ts
|
|
2327
2388
|
import { loadImage as loadImage2 } from "@napi-rs/canvas";
|
|
2389
|
+
import parseCssColor2 from "parse-css-color";
|
|
2328
2390
|
|
|
2329
2391
|
// src/jsx/emoji.ts
|
|
2330
2392
|
var emojiApis = {
|
|
@@ -2440,12 +2502,14 @@ function loadEmojiImage(style, char) {
|
|
|
2440
2502
|
return cached;
|
|
2441
2503
|
}
|
|
2442
2504
|
async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle) {
|
|
2505
|
+
const shadow = textShadow ? parseShadow(textShadow) : null;
|
|
2443
2506
|
for (const seg of segments) {
|
|
2444
2507
|
if (!seg.text) continue;
|
|
2445
2508
|
setFont(ctx, seg.fontSize, seg.fontFamily, seg.fontWeight, seg.fontStyle);
|
|
2446
2509
|
ctx.fillStyle = seg.color;
|
|
2447
2510
|
const x = offsetX + seg.x;
|
|
2448
2511
|
const y = offsetY + seg.y;
|
|
2512
|
+
const textAlpha = shadow ? getColorAlpha(seg.color) : 1;
|
|
2449
2513
|
const hasStroke = seg.textStrokeWidth !== void 0 && seg.textStrokeWidth > 0;
|
|
2450
2514
|
if (emojiStyle) {
|
|
2451
2515
|
await drawSegmentWithEmoji(
|
|
@@ -2458,6 +2522,14 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
|
|
|
2458
2522
|
hasStroke
|
|
2459
2523
|
);
|
|
2460
2524
|
} else if (seg.letterSpacing && seg.letterSpacing !== 0) {
|
|
2525
|
+
if (shadow) {
|
|
2526
|
+
drawShadowPass(
|
|
2527
|
+
ctx,
|
|
2528
|
+
shadow,
|
|
2529
|
+
textAlpha,
|
|
2530
|
+
() => drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing)
|
|
2531
|
+
);
|
|
2532
|
+
}
|
|
2461
2533
|
if (hasStroke) {
|
|
2462
2534
|
drawStrokeWithLetterSpacing(
|
|
2463
2535
|
ctx,
|
|
@@ -2471,8 +2543,13 @@ async function drawText(ctx, segments, offsetX, offsetY, textShadow, emojiStyle)
|
|
|
2471
2543
|
}
|
|
2472
2544
|
drawTextWithLetterSpacing(ctx, seg.text, x, y, seg.letterSpacing);
|
|
2473
2545
|
} else {
|
|
2474
|
-
if (
|
|
2475
|
-
|
|
2546
|
+
if (shadow) {
|
|
2547
|
+
drawShadowPass(
|
|
2548
|
+
ctx,
|
|
2549
|
+
shadow,
|
|
2550
|
+
textAlpha,
|
|
2551
|
+
() => ctx.fillText(seg.text, x, y)
|
|
2552
|
+
);
|
|
2476
2553
|
}
|
|
2477
2554
|
if (hasStroke) {
|
|
2478
2555
|
ctx.save();
|
|
@@ -2500,10 +2577,32 @@ async function drawSegmentWithEmoji(ctx, seg, x, y, textShadow, emojiStyle, hasS
|
|
|
2500
2577
|
seg.fontSize,
|
|
2501
2578
|
letterSpacing
|
|
2502
2579
|
);
|
|
2580
|
+
const shadow = textShadow ? parseShadow(textShadow) : null;
|
|
2581
|
+
const textAlpha = shadow ? getColorAlpha(seg.color) : 1;
|
|
2503
2582
|
for (const run of runs) {
|
|
2504
2583
|
if (run.kind === "text") {
|
|
2505
|
-
if (
|
|
2506
|
-
|
|
2584
|
+
if (shadow) {
|
|
2585
|
+
if (letterSpacing !== 0) {
|
|
2586
|
+
drawShadowPass(
|
|
2587
|
+
ctx,
|
|
2588
|
+
shadow,
|
|
2589
|
+
textAlpha,
|
|
2590
|
+
() => drawTextWithLetterSpacing(
|
|
2591
|
+
ctx,
|
|
2592
|
+
run.text,
|
|
2593
|
+
x + run.x,
|
|
2594
|
+
y,
|
|
2595
|
+
letterSpacing
|
|
2596
|
+
)
|
|
2597
|
+
);
|
|
2598
|
+
} else {
|
|
2599
|
+
drawShadowPass(
|
|
2600
|
+
ctx,
|
|
2601
|
+
shadow,
|
|
2602
|
+
textAlpha,
|
|
2603
|
+
() => ctx.fillText(run.text, x + run.x, y)
|
|
2604
|
+
);
|
|
2605
|
+
}
|
|
2507
2606
|
}
|
|
2508
2607
|
if (hasStroke) {
|
|
2509
2608
|
if (letterSpacing !== 0) {
|
|
@@ -2563,17 +2662,29 @@ function drawStrokeWithLetterSpacing(ctx, text, x, y, letterSpacing, strokeWidth
|
|
|
2563
2662
|
}
|
|
2564
2663
|
ctx.restore();
|
|
2565
2664
|
}
|
|
2566
|
-
function
|
|
2665
|
+
function parseShadow(shadow) {
|
|
2567
2666
|
const parts = shadow.match(
|
|
2568
2667
|
/(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(-?\d+(?:\.\d+)?)\s*(?:px)?\s+(.*)/
|
|
2569
2668
|
);
|
|
2570
|
-
if (!parts) return;
|
|
2669
|
+
if (!parts) return null;
|
|
2670
|
+
return {
|
|
2671
|
+
offsetX: parseFloat(parts[1]),
|
|
2672
|
+
offsetY: parseFloat(parts[2]),
|
|
2673
|
+
blur: parseFloat(parts[3]),
|
|
2674
|
+
color: parts[4].trim()
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
function getColorAlpha(color) {
|
|
2678
|
+
const parsed = parseCssColor2(color);
|
|
2679
|
+
return parsed ? parsed.alpha : 1;
|
|
2680
|
+
}
|
|
2681
|
+
function drawShadowPass(ctx, shadow, textAlpha, drawFn) {
|
|
2571
2682
|
ctx.save();
|
|
2572
|
-
ctx.
|
|
2573
|
-
ctx.
|
|
2574
|
-
ctx.
|
|
2575
|
-
ctx.
|
|
2576
|
-
|
|
2683
|
+
ctx.fillStyle = shadow.color;
|
|
2684
|
+
if (textAlpha < 1) ctx.globalAlpha *= textAlpha;
|
|
2685
|
+
if (shadow.blur > 0) ctx.filter = `blur(${shadow.blur / 2}px)`;
|
|
2686
|
+
ctx.translate(shadow.offsetX, shadow.offsetY);
|
|
2687
|
+
drawFn();
|
|
2577
2688
|
ctx.restore();
|
|
2578
2689
|
}
|
|
2579
2690
|
function drawTextDecoration(ctx, seg, offsetX, offsetY) {
|