@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 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
- function parseFontMetrics(data) {
264
- const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
265
- const view = new DataView(buf);
266
- if (buf.byteLength < 12) return null;
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 (buf.byteLength < 12 + numTables * 16) return null;
317
+ if (view.byteLength < 12 + numTables * 16) return null;
269
318
  let headOffset = -1;
270
- let os2Offset = -1;
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 === "OS/2") os2Offset = offset;
325
+ else if (tag === "hhea") hheaOffset = offset;
277
326
  }
278
- if (headOffset < 0 || os2Offset < 0) return null;
279
- if (buf.byteLength < headOffset + 20) return null;
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 (buf.byteLength < os2Offset + 74) return null;
282
- const sTypoAscender = view.getInt16(os2Offset + 68);
283
- const sTypoDescender = view.getInt16(os2Offset + 70);
284
- const sTypoLineGap = view.getInt16(os2Offset + 72);
285
- return { unitsPerEm, sTypoAscender, sTypoDescender, sTypoLineGap };
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 exact = metricsCache.get(`${family}:${weight}:${style}`);
304
- if (exact) return exact;
305
- for (const [key, metrics] of metricsCache) {
306
- if (key.startsWith(`${family}:`)) return metrics;
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.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
417
- fontDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
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 { sTypoAscender, sTypoDescender, sTypoLineGap, unitsPerEm } = fontMetrics;
731
- return (sTypoAscender - sTypoDescender + sTypoLineGap) / unitsPerEm * fontSize;
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 (textShadow) {
2475
- drawTextShadow(ctx, seg.text, x, y, textShadow);
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 (textShadow) {
2506
- drawTextShadow(ctx, run.text, x + run.x, y, textShadow);
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 drawTextShadow(ctx, text, x, y, shadow) {
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.shadowOffsetX = parseFloat(parts[1]);
2573
- ctx.shadowOffsetY = parseFloat(parts[2]);
2574
- ctx.shadowBlur = parseFloat(parts[3]);
2575
- ctx.shadowColor = parts[4].trim();
2576
- ctx.fillText(text, x, y);
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) {