circuit-to-canvas 0.0.21 → 0.0.22

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
@@ -648,34 +648,159 @@ function drawPcbBoard(params) {
648
648
  }
649
649
 
650
650
  // lib/drawer/elements/pcb-silkscreen.ts
651
+ import { applyToPoint as applyToPoint9 } from "transformation-matrix";
652
+
653
+ // lib/drawer/shapes/text/text.ts
654
+ import { lineAlphabet } from "@tscircuit/alphabet";
651
655
  import { applyToPoint as applyToPoint8 } from "transformation-matrix";
656
+
657
+ // lib/drawer/shapes/text/getAlphabetLayout.ts
658
+ var GLYPH_WIDTH_RATIO = 0.62;
659
+ var LETTER_SPACING_RATIO = 0.3;
660
+ var SPACE_WIDTH_RATIO = 1;
661
+ var STROKE_WIDTH_RATIO = 0.13;
662
+ function getAlphabetLayout(text, fontSize) {
663
+ const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
664
+ const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
665
+ const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
666
+ const characters = Array.from(text);
667
+ let width = 0;
668
+ characters.forEach((char, index) => {
669
+ const advance = char === " " ? spaceWidth : glyphWidth;
670
+ width += advance;
671
+ if (index < characters.length - 1) width += letterSpacing;
672
+ });
673
+ const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
674
+ return {
675
+ width,
676
+ height: fontSize,
677
+ glyphWidth,
678
+ letterSpacing,
679
+ spaceWidth,
680
+ strokeWidth
681
+ };
682
+ }
683
+
684
+ // lib/drawer/shapes/text/getTextStartPosition.ts
685
+ function getTextStartPosition(alignment, layout) {
686
+ const totalWidth = layout.width + layout.strokeWidth;
687
+ const totalHeight = layout.height + layout.strokeWidth;
688
+ let x = 0;
689
+ let y = 0;
690
+ if (alignment === "center") {
691
+ x = -totalWidth / 2;
692
+ } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
693
+ x = 0;
694
+ } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
695
+ x = -totalWidth;
696
+ }
697
+ if (alignment === "center") {
698
+ y = -totalHeight / 2;
699
+ } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
700
+ y = 0;
701
+ } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
702
+ y = -totalHeight;
703
+ } else {
704
+ y = 0;
705
+ }
706
+ return { x, y };
707
+ }
708
+
709
+ // lib/drawer/shapes/text/text.ts
710
+ var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
711
+ function strokeAlphabetText(ctx, text, layout, startX, startY) {
712
+ const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
713
+ const topY = startY;
714
+ const characters = Array.from(text);
715
+ let cursor = startX + strokeWidth / 2;
716
+ characters.forEach((char, index) => {
717
+ const lines = getGlyphLines(char);
718
+ const advance = char === " " ? spaceWidth : glyphWidth;
719
+ if (lines?.length) {
720
+ ctx.beginPath();
721
+ for (const line of lines) {
722
+ const x1 = cursor + line.x1 * glyphWidth;
723
+ const y1 = topY + (1 - line.y1) * height;
724
+ const x2 = cursor + line.x2 * glyphWidth;
725
+ const y2 = topY + (1 - line.y2) * height;
726
+ ctx.moveTo(x1, y1);
727
+ ctx.lineTo(x2, y2);
728
+ }
729
+ ctx.stroke();
730
+ }
731
+ cursor += advance;
732
+ if (index < characters.length - 1) {
733
+ cursor += letterSpacing;
734
+ }
735
+ });
736
+ }
737
+ function drawText(params) {
738
+ const {
739
+ ctx,
740
+ text,
741
+ x,
742
+ y,
743
+ fontSize,
744
+ color,
745
+ realToCanvasMat,
746
+ anchorAlignment,
747
+ rotation = 0
748
+ } = params;
749
+ if (!text) return;
750
+ const [canvasX, canvasY] = applyToPoint8(realToCanvasMat, [x, y]);
751
+ const scale2 = Math.abs(realToCanvasMat.a);
752
+ const scaledFontSize = fontSize * scale2;
753
+ const layout = getAlphabetLayout(text, scaledFontSize);
754
+ const startPos = getTextStartPosition(anchorAlignment, layout);
755
+ ctx.save();
756
+ ctx.translate(canvasX, canvasY);
757
+ if (rotation !== 0) {
758
+ ctx.rotate(-rotation * (Math.PI / 180));
759
+ }
760
+ ctx.lineWidth = layout.strokeWidth;
761
+ ctx.lineCap = "round";
762
+ ctx.lineJoin = "round";
763
+ ctx.strokeStyle = color;
764
+ strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
765
+ ctx.restore();
766
+ }
767
+
768
+ // lib/drawer/elements/pcb-silkscreen.ts
652
769
  function layerToSilkscreenColor(layer, colorMap) {
653
770
  return layer === "bottom" ? colorMap.silkscreen.bottom : colorMap.silkscreen.top;
654
771
  }
655
772
  function mapAnchorAlignment(alignment) {
656
773
  if (!alignment) return "center";
657
- if (alignment.includes("left")) return "left";
658
- if (alignment.includes("right")) return "right";
659
- return "center";
774
+ return alignment;
660
775
  }
661
776
  function drawPcbSilkscreenText(params) {
662
777
  const { ctx, text, realToCanvasMat, colorMap } = params;
778
+ const content = text.text ?? "";
779
+ if (!content) return;
663
780
  const color = layerToSilkscreenColor(text.layer, colorMap);
664
- const [x, y] = applyToPoint8(realToCanvasMat, [
781
+ const [x, y] = applyToPoint9(realToCanvasMat, [
665
782
  text.anchor_position.x,
666
783
  text.anchor_position.y
667
784
  ]);
668
- const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a);
785
+ const scale2 = Math.abs(realToCanvasMat.a);
786
+ const fontSize = (text.font_size ?? 1) * scale2;
669
787
  const rotation = text.ccw_rotation ?? 0;
788
+ const layout = getAlphabetLayout(content, fontSize);
789
+ const alignment = mapAnchorAlignment(text.anchor_alignment);
790
+ const startPos = getTextStartPosition(alignment, layout);
670
791
  ctx.save();
671
792
  ctx.translate(x, y);
672
793
  if (rotation !== 0) {
673
794
  ctx.rotate(-rotation * (Math.PI / 180));
674
795
  }
675
- ctx.font = `${fontSize}px sans-serif`;
676
- ctx.fillStyle = color;
677
- ctx.textAlign = mapAnchorAlignment(text.anchor_alignment);
678
- ctx.fillText(text.text, 0, 0);
796
+ if (text.layer === "bottom") {
797
+ ctx.scale(-1, 1);
798
+ }
799
+ ctx.lineWidth = layout.strokeWidth;
800
+ ctx.lineCap = "round";
801
+ ctx.lineJoin = "round";
802
+ ctx.strokeStyle = color;
803
+ strokeAlphabetText(ctx, content, layout, startPos.x, startPos.y);
679
804
  ctx.restore();
680
805
  }
681
806
  function drawPcbSilkscreenRect(params) {
@@ -771,7 +896,7 @@ function drawPcbCutout(params) {
771
896
  }
772
897
 
773
898
  // lib/drawer/elements/pcb-copper-pour.ts
774
- import { applyToPoint as applyToPoint9 } from "transformation-matrix";
899
+ import { applyToPoint as applyToPoint10 } from "transformation-matrix";
775
900
  function layerToColor3(layer, colorMap) {
776
901
  return colorMap.copper[layer] ?? colorMap.copper.top;
777
902
  }
@@ -780,7 +905,7 @@ function drawPcbCopperPour(params) {
780
905
  const color = layerToColor3(pour.layer, colorMap);
781
906
  ctx.save();
782
907
  if (pour.shape === "rect") {
783
- const [cx, cy] = applyToPoint9(realToCanvasMat, [
908
+ const [cx, cy] = applyToPoint10(realToCanvasMat, [
784
909
  pour.center.x,
785
910
  pour.center.y
786
911
  ]);
@@ -801,7 +926,7 @@ function drawPcbCopperPour(params) {
801
926
  if (pour.shape === "polygon") {
802
927
  if (pour.points && pour.points.length >= 3) {
803
928
  const canvasPoints = pour.points.map(
804
- (p) => applyToPoint9(realToCanvasMat, [p.x, p.y])
929
+ (p) => applyToPoint10(realToCanvasMat, [p.x, p.y])
805
930
  );
806
931
  const firstPoint = canvasPoints[0];
807
932
  if (!firstPoint) {
@@ -830,123 +955,6 @@ function drawPcbCopperPour(params) {
830
955
 
831
956
  // lib/drawer/elements/pcb-copper-text.ts
832
957
  import { applyToPoint as applyToPoint11 } from "transformation-matrix";
833
-
834
- // lib/drawer/shapes/text/text.ts
835
- import { lineAlphabet } from "@tscircuit/alphabet";
836
- import { applyToPoint as applyToPoint10 } from "transformation-matrix";
837
-
838
- // lib/drawer/shapes/text/getAlphabetLayout.ts
839
- var GLYPH_WIDTH_RATIO = 0.62;
840
- var LETTER_SPACING_RATIO = 0.3;
841
- var SPACE_WIDTH_RATIO = 1;
842
- var STROKE_WIDTH_RATIO = 0.13;
843
- function getAlphabetLayout(text, fontSize) {
844
- const glyphWidth = fontSize * GLYPH_WIDTH_RATIO;
845
- const letterSpacing = glyphWidth * LETTER_SPACING_RATIO;
846
- const spaceWidth = glyphWidth * SPACE_WIDTH_RATIO;
847
- const characters = Array.from(text);
848
- let width = 0;
849
- characters.forEach((char, index) => {
850
- const advance = char === " " ? spaceWidth : glyphWidth;
851
- width += advance;
852
- if (index < characters.length - 1) width += letterSpacing;
853
- });
854
- const strokeWidth = Math.max(fontSize * STROKE_WIDTH_RATIO, 0.35);
855
- return {
856
- width,
857
- height: fontSize,
858
- glyphWidth,
859
- letterSpacing,
860
- spaceWidth,
861
- strokeWidth
862
- };
863
- }
864
-
865
- // lib/drawer/shapes/text/getTextStartPosition.ts
866
- function getTextStartPosition(alignment, layout) {
867
- const totalWidth = layout.width + layout.strokeWidth;
868
- const totalHeight = layout.height + layout.strokeWidth;
869
- let x = 0;
870
- let y = 0;
871
- if (alignment === "center") {
872
- x = -totalWidth / 2;
873
- } else if (alignment === "top_left" || alignment === "bottom_left" || alignment === "center_left") {
874
- x = 0;
875
- } else if (alignment === "top_right" || alignment === "bottom_right" || alignment === "center_right") {
876
- x = -totalWidth;
877
- }
878
- if (alignment === "center") {
879
- y = -totalHeight / 2;
880
- } else if (alignment === "top_left" || alignment === "top_right" || alignment === "top_center") {
881
- y = 0;
882
- } else if (alignment === "bottom_left" || alignment === "bottom_right" || alignment === "bottom_center") {
883
- y = -totalHeight;
884
- } else {
885
- y = 0;
886
- }
887
- return { x, y };
888
- }
889
-
890
- // lib/drawer/shapes/text/text.ts
891
- var getGlyphLines = (char) => lineAlphabet[char] ?? lineAlphabet[char.toUpperCase()];
892
- function strokeAlphabetText(ctx, text, layout, startX, startY) {
893
- const { glyphWidth, letterSpacing, spaceWidth, height, strokeWidth } = layout;
894
- const topY = startY;
895
- const characters = Array.from(text);
896
- let cursor = startX + strokeWidth / 2;
897
- characters.forEach((char, index) => {
898
- const lines = getGlyphLines(char);
899
- const advance = char === " " ? spaceWidth : glyphWidth;
900
- if (lines?.length) {
901
- ctx.beginPath();
902
- for (const line of lines) {
903
- const x1 = cursor + line.x1 * glyphWidth;
904
- const y1 = topY + (1 - line.y1) * height;
905
- const x2 = cursor + line.x2 * glyphWidth;
906
- const y2 = topY + (1 - line.y2) * height;
907
- ctx.moveTo(x1, y1);
908
- ctx.lineTo(x2, y2);
909
- }
910
- ctx.stroke();
911
- }
912
- cursor += advance;
913
- if (index < characters.length - 1) {
914
- cursor += letterSpacing;
915
- }
916
- });
917
- }
918
- function drawText(params) {
919
- const {
920
- ctx,
921
- text,
922
- x,
923
- y,
924
- fontSize,
925
- color,
926
- realToCanvasMat,
927
- anchorAlignment,
928
- rotation = 0
929
- } = params;
930
- if (!text) return;
931
- const [canvasX, canvasY] = applyToPoint10(realToCanvasMat, [x, y]);
932
- const scale2 = Math.abs(realToCanvasMat.a);
933
- const scaledFontSize = fontSize * scale2;
934
- const layout = getAlphabetLayout(text, scaledFontSize);
935
- const startPos = getTextStartPosition(anchorAlignment, layout);
936
- ctx.save();
937
- ctx.translate(canvasX, canvasY);
938
- if (rotation !== 0) {
939
- ctx.rotate(-rotation * (Math.PI / 180));
940
- }
941
- ctx.lineWidth = layout.strokeWidth;
942
- ctx.lineCap = "round";
943
- ctx.lineJoin = "round";
944
- ctx.strokeStyle = color;
945
- strokeAlphabetText(ctx, text, layout, startPos.x, startPos.y);
946
- ctx.restore();
947
- }
948
-
949
- // lib/drawer/elements/pcb-copper-text.ts
950
958
  var DEFAULT_PADDING = { left: 0.2, right: 0.2, top: 0.2, bottom: 0.2 };
951
959
  function layerToCopperColor(layer, colorMap) {
952
960
  return colorMap.copper[layer] ?? colorMap.copper.top;
@@ -12,6 +12,12 @@ import { drawRect } from "../shapes/rect"
12
12
  import { drawCircle } from "../shapes/circle"
13
13
  import { drawLine } from "../shapes/line"
14
14
  import { drawPath } from "../shapes/path"
15
+ import {
16
+ getAlphabetLayout,
17
+ strokeAlphabetText,
18
+ getTextStartPosition,
19
+ type AnchorAlignment,
20
+ } from "../shapes/text"
15
21
 
16
22
  export interface DrawPcbSilkscreenTextParams {
17
23
  ctx: CanvasContext
@@ -54,13 +60,9 @@ function layerToSilkscreenColor(layer: string, colorMap: PcbColorMap): string {
54
60
  : colorMap.silkscreen.top
55
61
  }
56
62
 
57
- function mapAnchorAlignment(
58
- alignment?: string,
59
- ): "start" | "end" | "left" | "right" | "center" {
63
+ function mapAnchorAlignment(alignment?: string): AnchorAlignment {
60
64
  if (!alignment) return "center"
61
- if (alignment.includes("left")) return "left"
62
- if (alignment.includes("right")) return "right"
63
- return "center"
65
+ return alignment as AnchorAlignment
64
66
  }
65
67
 
66
68
  export function drawPcbSilkscreenText(
@@ -68,15 +70,22 @@ export function drawPcbSilkscreenText(
68
70
  ): void {
69
71
  const { ctx, text, realToCanvasMat, colorMap } = params
70
72
 
73
+ const content = text.text ?? ""
74
+ if (!content) return
75
+
71
76
  const color = layerToSilkscreenColor(text.layer, colorMap)
72
77
  const [x, y] = applyToPoint(realToCanvasMat, [
73
78
  text.anchor_position.x,
74
79
  text.anchor_position.y,
75
80
  ])
76
-
77
- const fontSize = (text.font_size ?? 1) * Math.abs(realToCanvasMat.a)
81
+ const scale = Math.abs(realToCanvasMat.a)
82
+ const fontSize = (text.font_size ?? 1) * scale
78
83
  const rotation = text.ccw_rotation ?? 0
79
84
 
85
+ const layout = getAlphabetLayout(content, fontSize)
86
+ const alignment = mapAnchorAlignment(text.anchor_alignment)
87
+ const startPos = getTextStartPosition(alignment, layout)
88
+
80
89
  ctx.save()
81
90
  ctx.translate(x, y)
82
91
 
@@ -85,10 +94,17 @@ export function drawPcbSilkscreenText(
85
94
  ctx.rotate(-rotation * (Math.PI / 180))
86
95
  }
87
96
 
88
- ctx.font = `${fontSize}px sans-serif`
89
- ctx.fillStyle = color
90
- ctx.textAlign = mapAnchorAlignment(text.anchor_alignment)
91
- ctx.fillText(text.text, 0, 0)
97
+ if (text.layer === "bottom") {
98
+ ctx.scale(-1, 1)
99
+ }
100
+
101
+ ctx.lineWidth = layout.strokeWidth
102
+ ctx.lineCap = "round"
103
+ ctx.lineJoin = "round"
104
+ ctx.strokeStyle = color
105
+
106
+ strokeAlphabetText(ctx, content, layout, startPos.x, startPos.y)
107
+
92
108
  ctx.restore()
93
109
  }
94
110
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "circuit-to-canvas",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.21",
4
+ "version": "0.0.22",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build": "tsup-node ./lib/index.ts --format esm --dts",
@@ -12,7 +12,7 @@
12
12
  "@biomejs/biome": "^2.3.8",
13
13
  "@napi-rs/canvas": "^0.1.84",
14
14
  "@resvg/resvg-js": "^2.6.2",
15
- "@tscircuit/alphabet": "^0.0.9",
15
+ "@tscircuit/alphabet": "^0.0.17",
16
16
  "@tscircuit/circuit-json-util": "^0.0.73",
17
17
  "@tscircuit/math-utils": "^0.0.29",
18
18
  "@types/bun": "latest",
@@ -5,7 +5,7 @@ import usbcFlashlightCircuit from "./usb-c-flashlight.json"
5
5
 
6
6
  const circuitElements = usbcFlashlightCircuit as AnyCircuitElement[]
7
7
 
8
- test.skip("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
8
+ test("USB-C flashlight - comprehensive comparison (circuit-to-canvas vs circuit-to-svg)", async () => {
9
9
  const stackedPng = await getStackedPngSvgComparison(circuitElements, {
10
10
  width: 400,
11
11
  height: 800,
@@ -64,6 +64,116 @@ test("draw silkscreen text bottom layer", async () => {
64
64
  )
65
65
  })
66
66
 
67
+ test("draw silkscreen text with rotation", async () => {
68
+ const canvas = createCanvas(100, 100)
69
+ const ctx = canvas.getContext("2d")
70
+ const drawer = new CircuitToCanvasDrawer(ctx)
71
+
72
+ ctx.fillStyle = "#1a1a1a"
73
+ ctx.fillRect(0, 0, 100, 100)
74
+
75
+ const text: PcbSilkscreenText = {
76
+ type: "pcb_silkscreen_text",
77
+ pcb_silkscreen_text_id: "text1",
78
+ pcb_component_id: "component1",
79
+ layer: "top",
80
+ text: "ROT45",
81
+ anchor_position: { x: 50, y: 50 },
82
+ anchor_alignment: "center",
83
+ font: "tscircuit2024",
84
+ font_size: 8,
85
+ ccw_rotation: 45,
86
+ }
87
+
88
+ drawer.drawElements([text])
89
+
90
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
91
+ import.meta.path,
92
+ "silkscreen-text-rotated",
93
+ )
94
+ })
95
+
96
+ test("draw silkscreen text bottom layer with rotation - tests transform order", async () => {
97
+ const canvas = createCanvas(150, 150)
98
+ const ctx = canvas.getContext("2d")
99
+ const drawer = new CircuitToCanvasDrawer(ctx)
100
+
101
+ ctx.fillStyle = "#1a1a1a"
102
+ ctx.fillRect(0, 0, 150, 150)
103
+
104
+ // This test verifies the transform order (translate -> rotate -> scale) is correct
105
+ // by testing bottom layer text with various rotations
106
+ const texts: PcbSilkscreenText[] = [
107
+ {
108
+ type: "pcb_silkscreen_text",
109
+ pcb_silkscreen_text_id: "text1",
110
+ pcb_component_id: "component1",
111
+ layer: "bottom",
112
+ text: "0",
113
+ anchor_position: { x: 75, y: 30 },
114
+ anchor_alignment: "center",
115
+ font: "tscircuit2024",
116
+ font_size: 6,
117
+ ccw_rotation: 0,
118
+ },
119
+ {
120
+ type: "pcb_silkscreen_text",
121
+ pcb_silkscreen_text_id: "text2",
122
+ pcb_component_id: "component1",
123
+ layer: "bottom",
124
+ text: "90",
125
+ anchor_position: { x: 120, y: 75 },
126
+ anchor_alignment: "center",
127
+ font: "tscircuit2024",
128
+ font_size: 6,
129
+ ccw_rotation: 90,
130
+ },
131
+ {
132
+ type: "pcb_silkscreen_text",
133
+ pcb_silkscreen_text_id: "text3",
134
+ pcb_component_id: "component1",
135
+ layer: "bottom",
136
+ text: "180",
137
+ anchor_position: { x: 75, y: 120 },
138
+ anchor_alignment: "center",
139
+ font: "tscircuit2024",
140
+ font_size: 6,
141
+ ccw_rotation: 180,
142
+ },
143
+ {
144
+ type: "pcb_silkscreen_text",
145
+ pcb_silkscreen_text_id: "text4",
146
+ pcb_component_id: "component1",
147
+ layer: "bottom",
148
+ text: "270",
149
+ anchor_position: { x: 30, y: 75 },
150
+ anchor_alignment: "center",
151
+ font: "tscircuit2024",
152
+ font_size: 6,
153
+ ccw_rotation: 270,
154
+ },
155
+ {
156
+ type: "pcb_silkscreen_text",
157
+ pcb_silkscreen_text_id: "text5",
158
+ pcb_component_id: "component1",
159
+ layer: "bottom",
160
+ text: "BTM",
161
+ anchor_position: { x: 75, y: 75 },
162
+ anchor_alignment: "center",
163
+ font: "tscircuit2024",
164
+ font_size: 8,
165
+ ccw_rotation: 45,
166
+ },
167
+ ]
168
+
169
+ drawer.drawElements(texts)
170
+
171
+ await expect(canvas.toBuffer("image/png")).toMatchPngSnapshot(
172
+ import.meta.path,
173
+ "silkscreen-text-bottom-rotated",
174
+ )
175
+ })
176
+
67
177
  test("draw silkscreen rect", async () => {
68
178
  const canvas = createCanvas(100, 100)
69
179
  const ctx = canvas.getContext("2d")
@@ -1,7 +1,9 @@
1
1
  import { createCanvas, loadImage } from "@napi-rs/canvas"
2
- import { Resvg } from "@resvg/resvg-js"
2
+ import { Resvg, type ResvgRenderOptions } from "@resvg/resvg-js"
3
3
  import * as fs from "node:fs"
4
+ import * as os from "node:os"
4
5
  import * as path from "node:path"
6
+ import tscircuitFont from "@tscircuit/alphabet/base64font"
5
7
 
6
8
  // Pre-generated label PNGs for common labels
7
9
  const labelPngCache: Map<string, Buffer> = new Map()
@@ -76,7 +78,44 @@ export const stackPngsVertically = async (
76
78
  return canvas.toBuffer("image/png")
77
79
  }
78
80
 
79
- export const svgToPng = (svg: string): Buffer => {
80
- const resvg = new Resvg(svg)
81
- return resvg.render().asPng()
81
+ export const svgToPng = (svgString: string): Buffer => {
82
+ const fontBuffer = Buffer.from(tscircuitFont, "base64")
83
+
84
+ let tempFontPath: string | undefined
85
+ let cleanupFn: (() => void) | undefined
86
+
87
+ try {
88
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "resvg-font-"))
89
+ tempFontPath = path.join(tempDir, "tscircuit-font.ttf")
90
+ fs.writeFileSync(tempFontPath, fontBuffer)
91
+
92
+ cleanupFn = () => {
93
+ try {
94
+ fs.unlinkSync(tempFontPath!)
95
+ } catch {
96
+ // Ignore errors during cleanup
97
+ }
98
+ }
99
+
100
+ const opts: ResvgRenderOptions = {
101
+ font: {
102
+ fontFiles: [tempFontPath],
103
+ loadSystemFonts: false,
104
+ defaultFontFamily: "TscircuitAlphabet",
105
+ monospaceFamily: "TscircuitAlphabet",
106
+ sansSerifFamily: "TscircuitAlphabet",
107
+ },
108
+ }
109
+
110
+ const resvg = new Resvg(svgString, opts)
111
+ const pngData = resvg.render()
112
+ const pngBuffer = pngData.asPng()
113
+
114
+ return Buffer.from(pngBuffer)
115
+ } finally {
116
+ // Clean up temporary font file
117
+ if (cleanupFn) {
118
+ cleanupFn()
119
+ }
120
+ }
82
121
  }