@effing/canvas 0.22.1 → 0.22.3

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.d.ts CHANGED
@@ -93,7 +93,7 @@ type RenderReactElementOptions = {
93
93
  *
94
94
  * await renderReactElement(ctx, <MyComponent />, { fonts: [myFont] });
95
95
  *
96
- * const png = canvas.encodeSync("png");
96
+ * const png = await canvas.encode("png");
97
97
  * ```
98
98
  *
99
99
  * @example HiDPI rendering (2x)
package/dist/index.js CHANGED
@@ -23,6 +23,69 @@ function renderLottieFrame(ctx, animation, frame) {
23
23
  // src/jsx/draw/index.ts
24
24
  import { loadImage as loadImage3 } from "@napi-rs/canvas";
25
25
 
26
+ // src/jsx/font.ts
27
+ import { GlobalFonts } from "@napi-rs/canvas";
28
+
29
+ // src/jsx/font-metrics.ts
30
+ function parseFontMetrics(data) {
31
+ const buf = data instanceof ArrayBuffer ? data : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
32
+ const view = new DataView(buf);
33
+ if (buf.byteLength < 12) return null;
34
+ const numTables = view.getUint16(4);
35
+ if (buf.byteLength < 12 + numTables * 16) return null;
36
+ let headOffset = -1;
37
+ let os2Offset = -1;
38
+ for (let i = 0; i < numTables; i++) {
39
+ const recordOffset = 12 + i * 16;
40
+ 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));
41
+ const offset = view.getUint32(recordOffset + 8);
42
+ if (tag === "head") headOffset = offset;
43
+ else if (tag === "OS/2") os2Offset = offset;
44
+ }
45
+ if (headOffset < 0 || os2Offset < 0) return null;
46
+ if (buf.byteLength < headOffset + 20) return null;
47
+ const unitsPerEm = view.getUint16(headOffset + 18);
48
+ if (buf.byteLength < os2Offset + 74) return null;
49
+ const sTypoAscender = view.getInt16(os2Offset + 68);
50
+ const sTypoDescender = view.getInt16(os2Offset + 70);
51
+ const sTypoLineGap = view.getInt16(os2Offset + 72);
52
+ return { unitsPerEm, sTypoAscender, sTypoDescender, sTypoLineGap };
53
+ }
54
+
55
+ // src/jsx/font.ts
56
+ var registeredFonts = /* @__PURE__ */ new Set();
57
+ var metricsCache = /* @__PURE__ */ new Map();
58
+ function registerFont(font) {
59
+ const key = `${font.name}:${font.weight}:${font.style}`;
60
+ if (registeredFonts.has(key)) return;
61
+ const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
62
+ GlobalFonts.register(buffer, font.name);
63
+ const metrics = parseFontMetrics(font.data);
64
+ if (metrics) {
65
+ metricsCache.set(key, metrics);
66
+ }
67
+ registeredFonts.add(key);
68
+ }
69
+ function getFontMetrics(family, weight, style) {
70
+ const exact = metricsCache.get(`${family}:${weight}:${style}`);
71
+ if (exact) return exact;
72
+ for (const [key, metrics] of metricsCache) {
73
+ if (key.startsWith(`${family}:`)) return metrics;
74
+ }
75
+ return null;
76
+ }
77
+ function registerFontFromPath(path, nameAlias) {
78
+ GlobalFonts.registerFromPath(path, nameAlias ?? "");
79
+ }
80
+ function registeredFamilies() {
81
+ return GlobalFonts.families.map((f) => f.family);
82
+ }
83
+ function ensureFontsRegistered(fonts) {
84
+ for (const font of fonts) {
85
+ registerFont(font);
86
+ }
87
+ }
88
+
26
89
  // src/jsx/language.ts
27
90
  function isEmoji(char) {
28
91
  const cp = char.codePointAt(0);
@@ -110,12 +173,19 @@ function measureText(text, fontSize, fontFamily, fontWeight = 400, fontStyle = "
110
173
  height: ascent + descent
111
174
  };
112
175
  }
113
- function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx) {
176
+ function measureTrimMetrics(fontSize, fontFamily, fontWeight, fontStyle, lineHeight, edge, ctx, fontMetrics) {
114
177
  const c = ctx ?? getScratchCtx();
115
178
  setFont(c, fontSize, fontFamily, fontWeight, fontStyle);
116
179
  const refMetrics = c.measureText("M");
117
- const fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
118
- const fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
180
+ let fontAscent;
181
+ let fontDescent;
182
+ if (fontMetrics) {
183
+ fontAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
184
+ fontDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
185
+ } else {
186
+ fontAscent = refMetrics.fontBoundingBoxAscent ?? refMetrics.actualBoundingBoxAscent ?? fontSize * 0.8;
187
+ fontDescent = refMetrics.fontBoundingBoxDescent ?? refMetrics.actualBoundingBoxDescent ?? fontSize * 0.2;
188
+ }
119
189
  const parts = edge.trim().split(/\s+/);
120
190
  const overEdge = parts[0] ?? "text";
121
191
  const underEdge = parts.length > 1 ? parts[1] : overEdge;
@@ -230,10 +300,12 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
230
300
  fontStyle,
231
301
  ctx
232
302
  );
303
+ const fontMetrics = getFontMetrics(fontFamily, fontWeight, fontStyle);
233
304
  const lineHeightPx = resolveLineHeight(
234
305
  style.lineHeight,
235
306
  fontSize,
236
- refMetrics
307
+ refMetrics,
308
+ fontMetrics
237
309
  );
238
310
  const letterSpacing = typeof style.letterSpacing === "number" ? style.letterSpacing : 0;
239
311
  const whiteSpace = style.whiteSpace ?? "normal";
@@ -338,10 +410,18 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
338
410
  fontStyle,
339
411
  ctx
340
412
  );
413
+ let baselineY;
414
+ if (fontMetrics) {
415
+ const typoAscent = fontMetrics.sTypoAscender / fontMetrics.unitsPerEm * fontSize;
416
+ const typoDescent = -fontMetrics.sTypoDescender / fontMetrics.unitsPerEm * fontSize;
417
+ baselineY = totalHeight + (lineHeightPx + typoAscent - typoDescent) / 2;
418
+ } else {
419
+ baselineY = totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2;
420
+ }
341
421
  segments.push({
342
422
  text: line,
343
423
  x,
344
- y: totalHeight + (lineHeightPx + metrics.ascent - metrics.descent) / 2,
424
+ y: baselineY,
345
425
  width: lineWidth,
346
426
  height: lineHeightPx,
347
427
  fontSize,
@@ -367,7 +447,8 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
367
447
  fontStyle,
368
448
  lineHeightPx,
369
449
  textBoxEdge,
370
- ctx
450
+ ctx,
451
+ fontMetrics
371
452
  );
372
453
  if (textBoxTrim === "trim-start" || textBoxTrim === "trim-both") {
373
454
  for (const seg of segments) {
@@ -385,9 +466,13 @@ function layoutText(text, style, maxWidth, ctx, emojiEnabled) {
385
466
  height: totalHeight
386
467
  };
387
468
  }
388
- function resolveLineHeight(lineHeight, fontSize, metrics) {
469
+ function resolveLineHeight(lineHeight, fontSize, canvasMetrics, fontMetrics) {
389
470
  if (lineHeight === void 0 || lineHeight === "normal") {
390
- return metrics ? metrics.ascent + metrics.descent : fontSize * 1.2;
471
+ if (fontMetrics) {
472
+ const { sTypoAscender, sTypoDescender, sTypoLineGap, unitsPerEm } = fontMetrics;
473
+ return (sTypoAscender - sTypoDescender + sTypoLineGap) / unitsPerEm * fontSize;
474
+ }
475
+ return canvasMetrics ? canvasMetrics.ascent + canvasMetrics.descent : fontSize * 1.2;
391
476
  }
392
477
  if (typeof lineHeight === "number") {
393
478
  return lineHeight > 5 ? lineHeight : lineHeight * fontSize;
@@ -764,8 +849,7 @@ function acquireOffscreen(w, h) {
764
849
  const canvas2 = ref.deref();
765
850
  if (canvas2) {
766
851
  const ctx = canvas2.getContext("2d");
767
- ctx.setTransform(1, 0, 0, 1, 0, 0);
768
- ctx.clearRect(0, 0, w, h);
852
+ ctx.reset();
769
853
  return [canvas2, ctx];
770
854
  }
771
855
  }
@@ -1618,9 +1702,11 @@ function drawSvgChild(ctx, child, inherited, color, defs = EMPTY_DEFS, vbW = 0,
1618
1702
  if (type === "defs" || type === "clipPath" || type === "mask" || type === "filter")
1619
1703
  return;
1620
1704
  const transform = type !== "g" ? props.transform : void 0;
1621
- if (transform) {
1705
+ const opacity = type !== "g" ? Number(props.opacity ?? 1) : 1;
1706
+ if (transform || opacity < 1) {
1622
1707
  ctx.save();
1623
- applySvgTransform(ctx, transform);
1708
+ if (transform) applySvgTransform(ctx, transform);
1709
+ if (opacity < 1) ctx.globalAlpha *= opacity;
1624
1710
  }
1625
1711
  const clipRef = parseUrlRef(props.clipPath ?? props["clip-path"]);
1626
1712
  const clipShapes = clipRef ? defs.clips.get(clipRef) : void 0;
@@ -1701,7 +1787,7 @@ function drawSvgChild(ctx, child, inherited, color, defs = EMPTY_DEFS, vbW = 0,
1701
1787
  releaseOffscreen(elemCanvas);
1702
1788
  }
1703
1789
  if (clipPath) ctx.restore();
1704
- if (transform) ctx.restore();
1790
+ if (transform || opacity < 1) ctx.restore();
1705
1791
  }
1706
1792
  function drawPath(ctx, props, inherited, color, defs) {
1707
1793
  const d = props.d;
@@ -1869,17 +1955,19 @@ function drawGroup(ctx, node, inherited, color, defs = EMPTY_DEFS, vbW = 0, vbH
1869
1955
  strokeLinejoin: merged.strokeLinejoin ?? merged["stroke-linejoin"] ?? inherited.strokeLinejoin,
1870
1956
  strokeOpacity: merged.strokeOpacity ?? merged["stroke-opacity"] ?? inherited.strokeOpacity
1871
1957
  };
1958
+ const opacity = Number(merged.opacity ?? 1);
1872
1959
  const transform = merged.transform;
1873
- if (transform) {
1960
+ if (transform || opacity < 1) {
1874
1961
  ctx.save();
1875
- applySvgTransform(ctx, transform);
1962
+ if (transform) applySvgTransform(ctx, transform);
1963
+ if (opacity < 1) ctx.globalAlpha *= opacity;
1876
1964
  }
1877
1965
  for (const child of children) {
1878
1966
  if (child != null && typeof child === "object") {
1879
1967
  drawSvgChild(ctx, child, groupInherited, color, defs, vbW, vbH);
1880
1968
  }
1881
1969
  }
1882
- if (transform) {
1970
+ if (transform || opacity < 1) {
1883
1971
  ctx.restore();
1884
1972
  }
1885
1973
  }
@@ -2521,28 +2609,6 @@ function parseAngle(value) {
2521
2609
  return parseFloat(value);
2522
2610
  }
2523
2611
 
2524
- // src/jsx/font.ts
2525
- import { GlobalFonts } from "@napi-rs/canvas";
2526
- var registeredFonts = /* @__PURE__ */ new Set();
2527
- function registerFont(font) {
2528
- const key = `${font.name}:${font.weight}:${font.style}`;
2529
- if (registeredFonts.has(key)) return;
2530
- const buffer = Buffer.isBuffer(font.data) ? font.data : Buffer.from(font.data);
2531
- GlobalFonts.register(buffer, font.name);
2532
- registeredFonts.add(key);
2533
- }
2534
- function registerFontFromPath(path, nameAlias) {
2535
- GlobalFonts.registerFromPath(path, nameAlias ?? "");
2536
- }
2537
- function registeredFamilies() {
2538
- return GlobalFonts.families.map((f) => f.family);
2539
- }
2540
- function ensureFontsRegistered(fonts) {
2541
- for (const font of fonts) {
2542
- registerFont(font);
2543
- }
2544
- }
2545
-
2546
2612
  // src/jsx/layout.ts
2547
2613
  import { loadImage as loadImage4 } from "@napi-rs/canvas";
2548
2614
 
@@ -3219,11 +3285,21 @@ async function buildNode(element, parentStyle, yogaNode, viewportWidth, viewport
3219
3285
  if (tagName === "svg") {
3220
3286
  if (props.width != null && style.width === void 0) {
3221
3287
  const v = props.width;
3222
- style.width = typeof v === "string" && v.endsWith("%") ? v : Number(v);
3288
+ if (typeof v === "string") {
3289
+ const resolved = resolveUnit(v, viewportWidth, viewportHeight, 16, 16);
3290
+ style.width = typeof resolved === "string" && resolved.endsWith("%") ? resolved : Number(resolved);
3291
+ } else {
3292
+ style.width = Number(v);
3293
+ }
3223
3294
  }
3224
3295
  if (props.height != null && style.height === void 0) {
3225
3296
  const v = props.height;
3226
- style.height = typeof v === "string" && v.endsWith("%") ? v : Number(v);
3297
+ if (typeof v === "string") {
3298
+ const resolved = resolveUnit(v, viewportWidth, viewportHeight, 16, 16);
3299
+ style.height = typeof resolved === "string" && resolved.endsWith("%") ? resolved : Number(resolved);
3300
+ } else {
3301
+ style.height = Number(v);
3302
+ }
3227
3303
  }
3228
3304
  const viewBox = props.viewBox;
3229
3305
  if (viewBox) {