@zebrash/core 0.1.3 → 0.1.4

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.
Files changed (79) hide show
  1. package/dist/assets/fonts-browser.d.ts +2 -0
  2. package/dist/assets/fonts-browser.d.ts.map +1 -1
  3. package/dist/assets/fonts-browser.js +6 -0
  4. package/dist/assets/fonts.d.ts +3 -6
  5. package/dist/assets/fonts.d.ts.map +1 -1
  6. package/dist/assets/fonts.js +15 -5
  7. package/dist/assets/index.d.ts +1 -1
  8. package/dist/assets/index.d.ts.map +1 -1
  9. package/dist/assets/index.js +1 -1
  10. package/dist/drawer-options.d.ts +20 -2
  11. package/dist/drawer-options.d.ts.map +1 -1
  12. package/dist/drawer-options.js +3 -1
  13. package/dist/drawer.d.ts +27 -0
  14. package/dist/drawer.d.ts.map +1 -1
  15. package/dist/drawer.js +96 -0
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +4 -0
  19. package/dist/svg/emitter.d.ts +76 -0
  20. package/dist/svg/emitter.d.ts.map +1 -0
  21. package/dist/svg/emitter.js +207 -0
  22. package/dist/svg/font_embed.d.ts +12 -0
  23. package/dist/svg/font_embed.d.ts.map +1 -0
  24. package/dist/svg/font_embed.js +61 -0
  25. package/dist/svg-drawers/barcode_128.d.ts +3 -0
  26. package/dist/svg-drawers/barcode_128.d.ts.map +1 -0
  27. package/dist/svg-drawers/barcode_128.js +74 -0
  28. package/dist/svg-drawers/barcode_2of5.d.ts +3 -0
  29. package/dist/svg-drawers/barcode_2of5.d.ts.map +1 -0
  30. package/dist/svg-drawers/barcode_2of5.js +36 -0
  31. package/dist/svg-drawers/barcode_39.d.ts +3 -0
  32. package/dist/svg-drawers/barcode_39.d.ts.map +1 -0
  33. package/dist/svg-drawers/barcode_39.js +34 -0
  34. package/dist/svg-drawers/barcode_aztec.d.ts +3 -0
  35. package/dist/svg-drawers/barcode_aztec.d.ts.map +1 -0
  36. package/dist/svg-drawers/barcode_aztec.js +48 -0
  37. package/dist/svg-drawers/barcode_datamatrix.d.ts +3 -0
  38. package/dist/svg-drawers/barcode_datamatrix.d.ts.map +1 -0
  39. package/dist/svg-drawers/barcode_datamatrix.js +44 -0
  40. package/dist/svg-drawers/barcode_ean13.d.ts +3 -0
  41. package/dist/svg-drawers/barcode_ean13.d.ts.map +1 -0
  42. package/dist/svg-drawers/barcode_ean13.js +76 -0
  43. package/dist/svg-drawers/barcode_paint_svg.d.ts +35 -0
  44. package/dist/svg-drawers/barcode_paint_svg.d.ts.map +1 -0
  45. package/dist/svg-drawers/barcode_paint_svg.js +144 -0
  46. package/dist/svg-drawers/barcode_pdf417.d.ts +3 -0
  47. package/dist/svg-drawers/barcode_pdf417.d.ts.map +1 -0
  48. package/dist/svg-drawers/barcode_pdf417.js +43 -0
  49. package/dist/svg-drawers/barcode_qr.d.ts +3 -0
  50. package/dist/svg-drawers/barcode_qr.d.ts.map +1 -0
  51. package/dist/svg-drawers/barcode_qr.js +39 -0
  52. package/dist/svg-drawers/graphic_box.d.ts +3 -0
  53. package/dist/svg-drawers/graphic_box.d.ts.map +1 -0
  54. package/dist/svg-drawers/graphic_box.js +64 -0
  55. package/dist/svg-drawers/graphic_circle.d.ts +3 -0
  56. package/dist/svg-drawers/graphic_circle.d.ts.map +1 -0
  57. package/dist/svg-drawers/graphic_circle.js +19 -0
  58. package/dist/svg-drawers/graphic_diagonal_line.d.ts +3 -0
  59. package/dist/svg-drawers/graphic_diagonal_line.d.ts.map +1 -0
  60. package/dist/svg-drawers/graphic_diagonal_line.js +36 -0
  61. package/dist/svg-drawers/graphic_field.d.ts +3 -0
  62. package/dist/svg-drawers/graphic_field.d.ts.map +1 -0
  63. package/dist/svg-drawers/graphic_field.js +72 -0
  64. package/dist/svg-drawers/index.d.ts +10 -0
  65. package/dist/svg-drawers/index.d.ts.map +1 -0
  66. package/dist/svg-drawers/index.js +41 -0
  67. package/dist/svg-drawers/maxicode.d.ts +3 -0
  68. package/dist/svg-drawers/maxicode.d.ts.map +1 -0
  69. package/dist/svg-drawers/maxicode.js +87 -0
  70. package/dist/svg-drawers/svg_element_drawer.d.ts +13 -0
  71. package/dist/svg-drawers/svg_element_drawer.d.ts.map +1 -0
  72. package/dist/svg-drawers/svg_element_drawer.js +2 -0
  73. package/dist/svg-drawers/text_field.d.ts +3 -0
  74. package/dist/svg-drawers/text_field.d.ts.map +1 -0
  75. package/dist/svg-drawers/text_field.js +264 -0
  76. package/dist/svg-drawers/transform.d.ts +20 -0
  77. package/dist/svg-drawers/transform.d.ts.map +1 -0
  78. package/dist/svg-drawers/transform.js +52 -0
  79. package/package.json +1 -1
@@ -11,5 +11,7 @@
11
11
  */
12
12
  export type FontKey = "HelveticaBold" | "DejavuSansMono" | "DejavuSansMonoBold" | "ZplGS";
13
13
  export declare function setFontBaseUrl(url: string): void;
14
+ export declare function getFontBaseUrl(): string;
15
+ export declare function getFontFilename(key: FontKey): string;
14
16
  export declare function getEmbeddedFont(key: FontKey): Promise<Uint8Array>;
15
17
  //# sourceMappingURL=fonts-browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fonts-browser.d.ts","sourceRoot":"","sources":["../../src/assets/fonts-browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC;AAW1F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEhD;AAKD,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBvE"}
1
+ {"version":3,"file":"fonts-browser.d.ts","sourceRoot":"","sources":["../../src/assets/fonts-browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC;AAW1F,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEpD;AAKD,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAoBvE"}
@@ -19,6 +19,12 @@ let baseUrl = "https://cdn.jsdelivr.net/gh/Fountain-Bio/zebrash-ts@main/src/asse
19
19
  export function setFontBaseUrl(url) {
20
20
  baseUrl = url.endsWith("/") ? url : `${url}/`;
21
21
  }
22
+ export function getFontBaseUrl() {
23
+ return baseUrl;
24
+ }
25
+ export function getFontFilename(key) {
26
+ return filenames[key];
27
+ }
22
28
  const cache = new Map();
23
29
  const inflight = new Map();
24
30
  export async function getEmbeddedFont(key) {
@@ -7,10 +7,7 @@
7
7
  */
8
8
  export type FontKey = "HelveticaBold" | "DejavuSansMono" | "DejavuSansMonoBold" | "ZplGS";
9
9
  export declare function getEmbeddedFont(key: FontKey): Promise<Uint8Array>;
10
- /**
11
- * No-op on the Node path — fonts are read from disk relative to the module
12
- * URL. Provided so the browser-platform-swapped equivalent has a parallel
13
- * API surface.
14
- */
15
- export declare function setFontBaseUrl(_url: string): void;
10
+ export declare function setFontBaseUrl(url: string): void;
11
+ export declare function getFontBaseUrl(): string;
12
+ export declare function getFontFilename(key: FontKey): string;
16
13
  //# sourceMappingURL=fonts.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../../src/assets/fonts.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC;AAqB1F,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAMvE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAEjD"}
1
+ {"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../../src/assets/fonts.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AAEH,MAAM,MAAM,OAAO,GAAG,eAAe,GAAG,gBAAgB,GAAG,oBAAoB,GAAG,OAAO,CAAC;AAqB1F,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAMvE;AAYD,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEhD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEpD"}
@@ -26,11 +26,21 @@ export async function getEmbeddedFont(key) {
26
26
  return data;
27
27
  }
28
28
  /**
29
- * No-op on the Node path — fonts are read from disk relative to the module
30
- * URL. Provided so the browser-platform-swapped equivalent has a parallel
31
- * API surface.
29
+ * URL where the bundled TTFs are mirrored. The Node loader reads fonts
30
+ * from disk for canvas rendering, but `drawLabelAsSvg` may emit
31
+ * `@font-face src: url(...)` references — those URLs need to resolve in
32
+ * whichever browser ultimately views the SVG. Defaults to the same
33
+ * jsdelivr URL the browser loader uses; override with `setFontBaseUrl` to
34
+ * point at a self-hosted mirror.
32
35
  */
33
- export function setFontBaseUrl(_url) {
34
- // intentionally empty on Node
36
+ let baseUrl = "https://cdn.jsdelivr.net/gh/Fountain-Bio/zebrash-ts@main/src/assets/fonts/";
37
+ export function setFontBaseUrl(url) {
38
+ baseUrl = url.endsWith("/") ? url : `${url}/`;
39
+ }
40
+ export function getFontBaseUrl() {
41
+ return baseUrl;
42
+ }
43
+ export function getFontFilename(key) {
44
+ return filenames[key];
35
45
  }
36
46
  //# sourceMappingURL=fonts.js.map
@@ -1,4 +1,4 @@
1
- export { type FontKey, getEmbeddedFont, setFontBaseUrl } from "./fonts.ts";
1
+ export { type FontKey, getEmbeddedFont, getFontBaseUrl, getFontFilename, setFontBaseUrl, } from "./fonts.ts";
2
2
  export { EmbeddedFontFamilies, registerEmbeddedFonts } from "./register.ts";
3
3
  export type { EmbeddedFontFamily } from "./register.ts";
4
4
  export declare const FONT0_NAME = "ZebrashHelveticaBold";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/assets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC5E,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,UAAU,yBAAyB,CAAC;AACjD,eAAO,MAAM,UAAU,0BAA0B,CAAC;AAElD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAC/D,eAAO,MAAM,4BAA4B,8BAA8B,CAAC;AACxE,eAAO,MAAM,kBAAkB,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/assets/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,eAAe,EACf,cAAc,EACd,eAAe,EACf,cAAc,GACf,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAC5E,YAAY,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,eAAO,MAAM,UAAU,yBAAyB,CAAC;AACjD,eAAO,MAAM,UAAU,0BAA0B,CAAC;AAElD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE,eAAO,MAAM,uBAAuB,0BAA0B,CAAC;AAC/D,eAAO,MAAM,4BAA4B,8BAA8B,CAAC;AACxE,eAAO,MAAM,kBAAkB,iBAAiB,CAAC"}
@@ -1,4 +1,4 @@
1
- export { getEmbeddedFont, setFontBaseUrl } from "./fonts.js";
1
+ export { getEmbeddedFont, getFontBaseUrl, getFontFilename, setFontBaseUrl, } from "./fonts.js";
2
2
  export { EmbeddedFontFamilies, registerEmbeddedFonts } from "./register.js";
3
3
  export const FONT0_NAME = "ZebrashHelveticaBold";
4
4
  export const FONT1_NAME = "ZebrashDejavuSansMono";
@@ -1,3 +1,18 @@
1
+ /**
2
+ * How `<text>` elements declare their font in `drawLabelAsSvg` output. Ignored
3
+ * by `drawLabelAsPng` which always uses platform-registered fonts.
4
+ *
5
+ * - `"url"` (default): emit `@font-face src: url("<cdn>/font.ttf")` so the
6
+ * browser can fetch the same TTFs the runtime font loader uses. Smallest
7
+ * SVGs; requires CDN access at view time.
8
+ * - `"embed"`: base64-inline the TTFs as `data:font/ttf;base64,…`. Fully
9
+ * self-contained; adds ~54KB (Helvetica only) to ~900KB (DejaVu pair) per
10
+ * SVG depending on which families the label references.
11
+ * - `"none"`: emit `font-family` only, no `@font-face` block. Renders only
12
+ * where the host already has the font (system-installed or supplied to
13
+ * the rasteriser directly, as in the test harness).
14
+ */
15
+ export type SvgFontEmbedMode = "url" | "embed" | "none";
1
16
  /**
2
17
  * Public drawer options — mirrors Go `drawers.DrawerOptions`.
3
18
  *
@@ -9,13 +24,16 @@ export interface DrawerOptions {
9
24
  dpmm: number;
10
25
  /** Render labels with inverted orientation upside-down. */
11
26
  enableInvertedLabels: boolean;
12
- /** Output 8-bit grayscale PNG preserving anti-aliasing instead of binary monochrome. */
27
+ /** Output 8-bit grayscale PNG preserving anti-aliasing instead of binary monochrome. PNG-only. */
13
28
  grayscaleOutput: boolean;
29
+ /** How fonts are referenced in SVG output. SVG-only. */
30
+ fontEmbed: SvgFontEmbedMode;
14
31
  }
15
32
  /**
16
33
  * Returns a copy of `options` with defaults applied for any zero/missing fields.
17
34
  *
18
- * Defaults: 101.6 mm × 203.2 mm at 8 dpmm, monochrome, no inversion.
35
+ * Defaults: 101.6 mm × 203.2 mm at 8 dpmm, monochrome, no inversion, SVG
36
+ * fonts referenced by URL.
19
37
  */
20
38
  export declare function withDefaults(options?: Partial<DrawerOptions>): DrawerOptions;
21
39
  //# sourceMappingURL=drawer-options.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drawer-options.d.ts","sourceRoot":"","sources":["../src/drawer-options.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,oBAAoB,EAAE,OAAO,CAAC;IAC9B,wFAAwF;IACxF,eAAe,EAAE,OAAO,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,aAAa,CAQhF"}
1
+ {"version":3,"file":"drawer-options.d.ts","sourceRoot":"","sources":["../src/drawer-options.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;AAExD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,2DAA2D;IAC3D,oBAAoB,EAAE,OAAO,CAAC;IAC9B,kGAAkG;IAClG,eAAe,EAAE,OAAO,CAAC;IACzB,wDAAwD;IACxD,SAAS,EAAE,gBAAgB,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,aAAa,CAShF"}
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Returns a copy of `options` with defaults applied for any zero/missing fields.
3
3
  *
4
- * Defaults: 101.6 mm × 203.2 mm at 8 dpmm, monochrome, no inversion.
4
+ * Defaults: 101.6 mm × 203.2 mm at 8 dpmm, monochrome, no inversion, SVG
5
+ * fonts referenced by URL.
5
6
  */
6
7
  export function withDefaults(options = {}) {
7
8
  return {
@@ -10,6 +11,7 @@ export function withDefaults(options = {}) {
10
11
  dpmm: options.dpmm || 8,
11
12
  enableInvertedLabels: options.enableInvertedLabels ?? false,
12
13
  grayscaleOutput: options.grayscaleOutput ?? false,
14
+ fontEmbed: options.fontEmbed ?? "url",
13
15
  };
14
16
  }
15
17
  //# sourceMappingURL=drawer-options.js.map
package/dist/drawer.d.ts CHANGED
@@ -9,7 +9,10 @@ import { type DrawerOptions } from "./drawer-options.ts";
9
9
  */
10
10
  export declare class Drawer {
11
11
  private readonly elementDrawers;
12
+ private svgElementDrawers;
12
13
  constructor(elementDrawers?: ElementDrawer[]);
14
+ /** Lazy SVG drawer set — only constructed when `drawLabelAsSvg` is called. */
15
+ private getSvgElementDrawers;
13
16
  /**
14
17
  * Renders `label` to a PNG `Buffer` according to `options`.
15
18
  *
@@ -23,5 +26,29 @@ export declare class Drawer {
23
26
  * - Encode as monochrome PNG by default, or 8-bit grayscale when requested.
24
27
  */
25
28
  drawLabelAsPng(label: LabelInfo, options?: Partial<DrawerOptions>): Promise<Uint8Array>;
29
+ /**
30
+ * Renders `label` to an SVG document string according to `options`.
31
+ *
32
+ * Behaviour parity with `drawLabelAsPng` for everything geometric:
33
+ * - Apply `withDefaults` to the options.
34
+ * - Compute `imageWidth = min(labelWidth, label.printWidth)`.
35
+ * - Reverse-print elements are wrapped in a
36
+ * `<g style="mix-blend-mode: difference">` group, which is
37
+ * mathematically equivalent to the XOR composite the PNG path uses
38
+ * when the canvas only holds black-on-white (the monochrome case).
39
+ * - If the print width is narrower than the label, or the label is
40
+ * inverted, content is centred and optionally rotated 180° via outer
41
+ * `<g transform>` wrappers — same arithmetic as the canvas pipeline.
42
+ *
43
+ * SVG-specific behaviour (controlled by `options.fontEmbed`):
44
+ * - `"url"` (default): emits one `@font-face url(...)` per used font,
45
+ * pointing at the same CDN base the runtime font loader uses.
46
+ * - `"embed"`: base64-inlines the TTF bytes (large, fully self-contained).
47
+ * - `"none"`: omits `@font-face` entirely (caller supplies fonts to the
48
+ * rasteriser, or the host has them installed).
49
+ *
50
+ * `grayscaleOutput` is ignored (PNG-only encoding concern).
51
+ */
52
+ drawLabelAsSvg(label: LabelInfo, options?: Partial<DrawerOptions>): Promise<string>;
26
53
  }
27
54
  //# sourceMappingURL=drawer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../src/drawer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAAE,KAAK,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AAwCvE;;;;;GAKG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAkB;gBAErC,cAAc,CAAC,EAAE,aAAa,EAAE;IAI5C;;;;;;;;;;;OAWG;IACG,cAAc,CAClB,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GACnC,OAAO,CAAC,UAAU,CAAC;CAuEvB"}
1
+ {"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../src/drawer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD,OAAO,EAAE,KAAK,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AA2CvE;;;;;GAKG;AACH,qBAAa,MAAM;IACjB,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAkB;IACjD,OAAO,CAAC,iBAAiB,CAAmC;gBAEhD,cAAc,CAAC,EAAE,aAAa,EAAE;IAI5C,8EAA8E;IAC9E,OAAO,CAAC,oBAAoB;IAO5B;;;;;;;;;;;OAWG;IACG,cAAc,CAClB,KAAK,EAAE,SAAS,EAChB,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GACnC,OAAO,CAAC,UAAU,CAAC;IAwEtB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,cAAc,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,OAAO,CAAC,aAAa,CAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAkF9F"}
package/dist/drawer.js CHANGED
@@ -3,6 +3,9 @@ import { DrawerState } from "./drawers/drawer_state.js";
3
3
  import { defaultElementDrawers as defaultElementDrawersLazy } from "./drawers/index.js";
4
4
  import { colorWhite, encodeGrayscale, encodeMonochrome, reversePrint, zerofill, } from "./images/index.js";
5
5
  import { platform } from "./platform.js";
6
+ import { defaultSvgElementDrawers } from "./svg-drawers/index.js";
7
+ import { SvgEmitter } from "./svg/emitter.js";
8
+ import { buildFontFaceCss } from "./svg/font_embed.js";
6
9
  function isReversePrintable(element) {
7
10
  if (typeof element !== "object" || element === null)
8
11
  return false;
@@ -25,9 +28,17 @@ function fillWhite(ctx, width, height) {
25
28
  */
26
29
  export class Drawer {
27
30
  elementDrawers;
31
+ svgElementDrawers = null;
28
32
  constructor(elementDrawers) {
29
33
  this.elementDrawers = elementDrawers ?? defaultElementDrawersLazy();
30
34
  }
35
+ /** Lazy SVG drawer set — only constructed when `drawLabelAsSvg` is called. */
36
+ getSvgElementDrawers() {
37
+ if (this.svgElementDrawers === null) {
38
+ this.svgElementDrawers = defaultSvgElementDrawers();
39
+ }
40
+ return this.svgElementDrawers;
41
+ }
31
42
  /**
32
43
  * Renders `label` to a PNG `Buffer` according to `options`.
33
44
  *
@@ -98,5 +109,90 @@ export class Drawer {
98
109
  ? encodeGrayscale(finalCtx.canvas)
99
110
  : encodeMonochrome(finalCtx.canvas);
100
111
  }
112
+ /**
113
+ * Renders `label` to an SVG document string according to `options`.
114
+ *
115
+ * Behaviour parity with `drawLabelAsPng` for everything geometric:
116
+ * - Apply `withDefaults` to the options.
117
+ * - Compute `imageWidth = min(labelWidth, label.printWidth)`.
118
+ * - Reverse-print elements are wrapped in a
119
+ * `<g style="mix-blend-mode: difference">` group, which is
120
+ * mathematically equivalent to the XOR composite the PNG path uses
121
+ * when the canvas only holds black-on-white (the monochrome case).
122
+ * - If the print width is narrower than the label, or the label is
123
+ * inverted, content is centred and optionally rotated 180° via outer
124
+ * `<g transform>` wrappers — same arithmetic as the canvas pipeline.
125
+ *
126
+ * SVG-specific behaviour (controlled by `options.fontEmbed`):
127
+ * - `"url"` (default): emits one `@font-face url(...)` per used font,
128
+ * pointing at the same CDN base the runtime font loader uses.
129
+ * - `"embed"`: base64-inlines the TTF bytes (large, fully self-contained).
130
+ * - `"none"`: omits `@font-face` entirely (caller supplies fonts to the
131
+ * rasteriser, or the host has them installed).
132
+ *
133
+ * `grayscaleOutput` is ignored (PNG-only encoding concern).
134
+ */
135
+ async drawLabelAsSvg(label, options = {}) {
136
+ const opts = withDefaults(options);
137
+ const state = new DrawerState();
138
+ const labelWidth = Math.ceil(opts.labelWidthMm * opts.dpmm);
139
+ const imageHeight = Math.ceil(opts.labelHeightMm * opts.dpmm);
140
+ let imageWidth = labelWidth;
141
+ if (label.printWidth > 0) {
142
+ imageWidth = Math.min(labelWidth, label.printWidth);
143
+ }
144
+ const emitter = new SvgEmitter();
145
+ // White background. SVG needs an explicit rect — `<svg>` itself is
146
+ // transparent.
147
+ emitter.rect(0, 0, labelWidth, imageHeight, colorWhite);
148
+ // Mirror the PNG path's outer composition: if `imageWidth < labelWidth`
149
+ // or the label is inverted, wrap content in groups that translate /
150
+ // rotate the inner drawing into place.
151
+ const invertLabel = opts.enableInvertedLabels && label.inverted;
152
+ emitter.save();
153
+ if (invertLabel) {
154
+ // Rotate 180° about the canvas centre — equivalent to the canvas
155
+ // pipeline's `translate(W,H); scale(-1,-1)`.
156
+ emitter.translate(labelWidth, imageHeight);
157
+ emitter.scale(-1, -1);
158
+ }
159
+ if (imageWidth !== labelWidth) {
160
+ emitter.translate(Math.floor((labelWidth - imageWidth) / 2), 0);
161
+ }
162
+ const svgDrawers = this.getSvgElementDrawers();
163
+ let reverseFilterDefined = false;
164
+ for (const element of label.elements) {
165
+ const reverse = isReversePrintable(element) && isReversePrintActive(element);
166
+ if (reverse) {
167
+ if (!reverseFilterDefined) {
168
+ // The canvas reverse-print operation is `dst = 255 - dst` wherever
169
+ // the element drew (regardless of the colour the element drew in).
170
+ // To replicate this with SVG blend modes, we map the element's
171
+ // drawn pixels to white via `feFlood` masked by `SourceAlpha`, then
172
+ // composite the result with `mix-blend-mode: difference`:
173
+ // white over black dst → |255 − 0| = 255 (white)
174
+ // white over white dst → |255 − 255| = 0 (black)
175
+ // i.e. inversion of dst wherever the element drew. The element
176
+ // drawers continue to use black ink — the filter handles the
177
+ // colour swap so no per-drawer change is required.
178
+ emitter.defineFragment("zb-reverse", '<filter id="zb-reverse" x="0%" y="0%" width="100%" height="100%">' +
179
+ '<feFlood flood-color="#ffffff" result="flood"/>' +
180
+ '<feComposite in="flood" in2="SourceAlpha" operator="in"/>' +
181
+ "</filter>");
182
+ reverseFilterDefined = true;
183
+ }
184
+ emitter.pushGroup('filter="url(#zb-reverse)" style="mix-blend-mode:difference;isolation:auto"');
185
+ }
186
+ for (const drawer of svgDrawers) {
187
+ await drawer.draw(emitter, element, opts, state);
188
+ }
189
+ if (reverse) {
190
+ emitter.popGroup();
191
+ }
192
+ }
193
+ emitter.restore();
194
+ const fontFaceCss = await buildFontFaceCss(emitter.usedFonts, opts.fontEmbed);
195
+ return emitter.toSvg(labelWidth, imageHeight, fontFaceCss);
196
+ }
101
197
  }
102
198
  //# sourceMappingURL=drawer.js.map
package/dist/index.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export { Parser } from "./parser.ts";
2
2
  export { Drawer } from "./drawer.ts";
3
- export type { DrawerOptions } from "./drawer-options.ts";
3
+ export type { DrawerOptions, SvgFontEmbedMode } from "./drawer-options.ts";
4
4
  export { withDefaults as drawerOptionsWithDefaults } from "./drawer-options.ts";
5
5
  export type { LabelInfo } from "./elements/index.ts";
6
+ export { SvgEmitter } from "./svg/emitter.ts";
7
+ export type { SvgElementDrawer } from "./svg-drawers/index.ts";
8
+ export { defaultSvgElementDrawers } from "./svg-drawers/index.ts";
6
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,YAAY,IAAI,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,YAAY,IAAI,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC/D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -5,4 +5,8 @@
5
5
  export { Parser } from "./parser.js";
6
6
  export { Drawer } from "./drawer.js";
7
7
  export { withDefaults as drawerOptionsWithDefaults } from "./drawer-options.js";
8
+ // SVG output surface — for advanced consumers who want to register custom
9
+ // per-element SVG emitters.
10
+ export { SvgEmitter } from "./svg/emitter.js";
11
+ export { defaultSvgElementDrawers } from "./svg-drawers/index.js";
8
12
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,76 @@
1
+ import type { FontKey } from "../assets/fonts.ts";
2
+ /**
3
+ * SVG-side analog of `CanvasRenderingContext2D` used by the per-element SVG
4
+ * drawers. Pure string assembly — no platform dependency, identical
5
+ * behaviour on Node and browser.
6
+ *
7
+ * The transform model mirrors canvas: `save` snapshots the current state and
8
+ * `restore` rolls back to it. `translate`/`rotate`/`scale` and `pushGroup`
9
+ * each emit a `<g>` open whose matching close is deferred until the next
10
+ * `restore` (or `popGroup`). This lets the SVG drawers stay near
11
+ * line-for-line ports of their canvas siblings.
12
+ */
13
+ export declare class SvgEmitter {
14
+ private body;
15
+ private defs;
16
+ private readonly definedSymbols;
17
+ private readonly saveStack;
18
+ private openGroups;
19
+ private readonly fonts;
20
+ save(): void;
21
+ restore(): void;
22
+ translate(tx: number, ty: number): void;
23
+ rotate(radians: number): void;
24
+ scale(sx: number, sy: number): void;
25
+ /** Open a group with arbitrary attributes (style, mask, etc.). */
26
+ pushGroup(attrs: string): void;
27
+ popGroup(): void;
28
+ rect(x: number, y: number, width: number, height: number, fill: string): void;
29
+ /**
30
+ * Stroked rectangle outline. Width and height are the outer extents; the
31
+ * stroke is centred on the path so the caller is responsible for any
32
+ * inset that mirrors the canvas drawer's `border / 2` adjustment.
33
+ */
34
+ rectStroke(x: number, y: number, width: number, height: number, stroke: string, strokeWidth: number): void;
35
+ /**
36
+ * Two-rect "donut" emitted as a single `<path>` with `fill-rule: evenodd`.
37
+ * Mirrors the canvas drawer's rounded-rect-with-inner-cutout pattern used
38
+ * by `^GB` (graphic box) when `cornerRounding > 0`.
39
+ */
40
+ roundedDonut(x: number, y: number, w: number, h: number, rOuter: number, innerX: number, innerY: number, innerW: number, innerH: number, rInner: number, fill: string): void;
41
+ roundedRect(x: number, y: number, w: number, h: number, r: number, fill: string): void;
42
+ polygon(points: ReadonlyArray<readonly [number, number]>, fill: string): void;
43
+ circleStroke(cx: number, cy: number, r: number, stroke: string, strokeWidth: number): void;
44
+ /** Reuse a previously-defined `<symbol id>` at (x, y). Used by maxicode. */
45
+ use(id: string, x: number, y: number): void;
46
+ /**
47
+ * Define a `<symbol>` once, by id, in the SVG `<defs>`. No-ops on repeat
48
+ * calls with the same id, so callers can lazily ensure a symbol exists.
49
+ */
50
+ defineSymbol(id: string, fragment: string): void;
51
+ /**
52
+ * Define a raw `<defs>` fragment once, by id. The `<defs>` body is kept
53
+ * unique by tracking ids in `definedSymbols` (despite the name —
54
+ * tracking-set is shared so any defs id is global to the document).
55
+ */
56
+ defineFragment(id: string, fragment: string): void;
57
+ text(x: number, y: number, str: string, opts: {
58
+ fontFamily: string;
59
+ fontSize: number;
60
+ fontKey?: FontKey | undefined;
61
+ anchor?: "start" | "middle" | "end";
62
+ baseline?: "alphabetic" | "hanging" | "middle" | "text-top";
63
+ fill?: string;
64
+ }): void;
65
+ image(href: string, x: number, y: number, width: number, height: number): void;
66
+ /** Set of `FontKey`s referenced by `text` calls so far. */
67
+ get usedFonts(): ReadonlySet<FontKey>;
68
+ /**
69
+ * Wrap the accumulated body in an `<svg>` root and return the document.
70
+ * `extraDefs` is interpolated inside `<defs>` (use it for `@font-face`).
71
+ * The internal `<symbol>` defs collected via `defineSymbol` are emitted
72
+ * unconditionally.
73
+ */
74
+ toSvg(width: number, height: number, extraDefs: string): string;
75
+ }
76
+ //# sourceMappingURL=emitter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emitter.d.ts","sourceRoot":"","sources":["../../src/svg/emitter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,207 @@
1
+ /**
2
+ * SVG-side analog of `CanvasRenderingContext2D` used by the per-element SVG
3
+ * drawers. Pure string assembly — no platform dependency, identical
4
+ * behaviour on Node and browser.
5
+ *
6
+ * The transform model mirrors canvas: `save` snapshots the current state and
7
+ * `restore` rolls back to it. `translate`/`rotate`/`scale` and `pushGroup`
8
+ * each emit a `<g>` open whose matching close is deferred until the next
9
+ * `restore` (or `popGroup`). This lets the SVG drawers stay near
10
+ * line-for-line ports of their canvas siblings.
11
+ */
12
+ export class SvgEmitter {
13
+ body = "";
14
+ defs = "";
15
+ definedSymbols = new Set();
16
+ saveStack = [];
17
+ openGroups = 0;
18
+ fonts = new Set();
19
+ save() {
20
+ this.saveStack.push(this.openGroups);
21
+ }
22
+ restore() {
23
+ const target = this.saveStack.pop() ?? 0;
24
+ while (this.openGroups > target) {
25
+ this.body += "</g>";
26
+ this.openGroups -= 1;
27
+ }
28
+ }
29
+ translate(tx, ty) {
30
+ if (tx === 0 && ty === 0)
31
+ return;
32
+ this.body += `<g transform="translate(${n(tx)} ${n(ty)})">`;
33
+ this.openGroups += 1;
34
+ }
35
+ rotate(radians) {
36
+ if (radians === 0)
37
+ return;
38
+ const degrees = (radians * 180) / Math.PI;
39
+ this.body += `<g transform="rotate(${n(degrees)})">`;
40
+ this.openGroups += 1;
41
+ }
42
+ scale(sx, sy) {
43
+ if (sx === 1 && sy === 1)
44
+ return;
45
+ this.body += `<g transform="scale(${n(sx)} ${n(sy)})">`;
46
+ this.openGroups += 1;
47
+ }
48
+ /** Open a group with arbitrary attributes (style, mask, etc.). */
49
+ pushGroup(attrs) {
50
+ this.body += attrs.length > 0 ? `<g ${attrs}>` : "<g>";
51
+ this.openGroups += 1;
52
+ }
53
+ popGroup() {
54
+ if (this.openGroups === 0)
55
+ return;
56
+ this.body += "</g>";
57
+ this.openGroups -= 1;
58
+ }
59
+ rect(x, y, width, height, fill) {
60
+ if (width <= 0 || height <= 0)
61
+ return;
62
+ this.body += `<rect x="${n(x)}" y="${n(y)}" width="${n(width)}" height="${n(height)}" fill="${fill}"/>`;
63
+ }
64
+ /**
65
+ * Stroked rectangle outline. Width and height are the outer extents; the
66
+ * stroke is centred on the path so the caller is responsible for any
67
+ * inset that mirrors the canvas drawer's `border / 2` adjustment.
68
+ */
69
+ rectStroke(x, y, width, height, stroke, strokeWidth) {
70
+ this.body += `<rect x="${n(x)}" y="${n(y)}" width="${n(width)}" height="${n(height)}" fill="none" stroke="${stroke}" stroke-width="${n(strokeWidth)}"/>`;
71
+ }
72
+ /**
73
+ * Two-rect "donut" emitted as a single `<path>` with `fill-rule: evenodd`.
74
+ * Mirrors the canvas drawer's rounded-rect-with-inner-cutout pattern used
75
+ * by `^GB` (graphic box) when `cornerRounding > 0`.
76
+ */
77
+ roundedDonut(x, y, w, h, rOuter, innerX, innerY, innerW, innerH, rInner, fill) {
78
+ const outer = roundedRectPath(x, y, w, h, rOuter);
79
+ if (innerW <= 0 || innerH <= 0 || rInner <= 0) {
80
+ this.body += `<path d="${outer}" fill="${fill}"/>`;
81
+ return;
82
+ }
83
+ const inner = roundedRectPath(innerX, innerY, innerW, innerH, rInner);
84
+ this.body += `<path d="${outer} ${inner}" fill="${fill}" fill-rule="evenodd"/>`;
85
+ }
86
+ roundedRect(x, y, w, h, r, fill) {
87
+ this.body += `<path d="${roundedRectPath(x, y, w, h, r)}" fill="${fill}"/>`;
88
+ }
89
+ polygon(points, fill) {
90
+ if (points.length === 0)
91
+ return;
92
+ const pts = points.map(([x, y]) => `${n(x)},${n(y)}`).join(" ");
93
+ this.body += `<polygon points="${pts}" fill="${fill}"/>`;
94
+ }
95
+ circleStroke(cx, cy, r, stroke, strokeWidth) {
96
+ this.body += `<circle cx="${n(cx)}" cy="${n(cy)}" r="${n(r)}" fill="none" stroke="${stroke}" stroke-width="${n(strokeWidth)}"/>`;
97
+ }
98
+ /** Reuse a previously-defined `<symbol id>` at (x, y). Used by maxicode. */
99
+ use(id, x, y) {
100
+ this.body += `<use href="#${id}" x="${n(x)}" y="${n(y)}"/>`;
101
+ }
102
+ /**
103
+ * Define a `<symbol>` once, by id, in the SVG `<defs>`. No-ops on repeat
104
+ * calls with the same id, so callers can lazily ensure a symbol exists.
105
+ */
106
+ defineSymbol(id, fragment) {
107
+ if (this.definedSymbols.has(id))
108
+ return;
109
+ this.definedSymbols.add(id);
110
+ this.defs += `<symbol id="${id}" overflow="visible">${fragment}</symbol>`;
111
+ }
112
+ /**
113
+ * Define a raw `<defs>` fragment once, by id. The `<defs>` body is kept
114
+ * unique by tracking ids in `definedSymbols` (despite the name —
115
+ * tracking-set is shared so any defs id is global to the document).
116
+ */
117
+ defineFragment(id, fragment) {
118
+ if (this.definedSymbols.has(id))
119
+ return;
120
+ this.definedSymbols.add(id);
121
+ this.defs += fragment;
122
+ }
123
+ text(x, y, str, opts) {
124
+ if (opts.fontKey !== undefined)
125
+ this.fonts.add(opts.fontKey);
126
+ const anchor = opts.anchor ?? "start";
127
+ const fill = opts.fill ?? "#000000";
128
+ const baseline = opts.baseline ?? "alphabetic";
129
+ // SVG `dominant-baseline: alphabetic` is the canvas default; emit only
130
+ // when overridden so the attribute set stays terse.
131
+ const baselineAttr = baseline === "alphabetic" ? "" : ` dominant-baseline="${baseline}"`;
132
+ this.body += `<text x="${n(x)}" y="${n(y)}" font-family="${escapeAttr(opts.fontFamily)}" font-size="${n(opts.fontSize)}" fill="${fill}" text-anchor="${anchor}"${baselineAttr}>${escapeText(str)}</text>`;
133
+ }
134
+ image(href, x, y, width, height) {
135
+ if (width <= 0 || height <= 0)
136
+ return;
137
+ // `image-rendering: pixelated` keeps `^GF` bitmaps crisp when the SVG
138
+ // is scaled up — matches the nearest-neighbor look of the PNG path.
139
+ this.body += `<image href="${escapeAttr(href)}" x="${n(x)}" y="${n(y)}" width="${n(width)}" height="${n(height)}" preserveAspectRatio="none" style="image-rendering:pixelated"/>`;
140
+ }
141
+ /** Set of `FontKey`s referenced by `text` calls so far. */
142
+ get usedFonts() {
143
+ return this.fonts;
144
+ }
145
+ /**
146
+ * Wrap the accumulated body in an `<svg>` root and return the document.
147
+ * `extraDefs` is interpolated inside `<defs>` (use it for `@font-face`).
148
+ * The internal `<symbol>` defs collected via `defineSymbol` are emitted
149
+ * unconditionally.
150
+ */
151
+ toSvg(width, height, extraDefs) {
152
+ // Close any groups the caller forgot to balance — defensive, mirrors
153
+ // canvas's tolerance for unbalanced save/restore.
154
+ while (this.openGroups > 0) {
155
+ this.body += "</g>";
156
+ this.openGroups -= 1;
157
+ }
158
+ const defsBody = `${extraDefs}${this.defs}`;
159
+ const defsBlock = defsBody.length > 0 ? `<defs>${defsBody}</defs>` : "";
160
+ return (`<svg xmlns="http://www.w3.org/2000/svg" width="${n(width)}" height="${n(height)}" viewBox="0 0 ${n(width)} ${n(height)}">` +
161
+ defsBlock +
162
+ this.body +
163
+ "</svg>");
164
+ }
165
+ }
166
+ /**
167
+ * Format a number for SVG attribute output. Trims trailing zeros and emits
168
+ * integers without a decimal point. 3 decimal places balances precision
169
+ * (≤ 1/1000 of a pixel) against output size.
170
+ */
171
+ function n(v) {
172
+ if (Number.isInteger(v))
173
+ return v.toString();
174
+ const s = v.toFixed(3);
175
+ // Trim trailing zeros and a dangling decimal point.
176
+ return s.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, "");
177
+ }
178
+ function escapeText(s) {
179
+ return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
180
+ }
181
+ function escapeAttr(s) {
182
+ return s
183
+ .replaceAll("&", "&amp;")
184
+ .replaceAll("<", "&lt;")
185
+ .replaceAll(">", "&gt;")
186
+ .replaceAll('"', "&quot;");
187
+ }
188
+ function roundedRectPath(x, y, w, h, r) {
189
+ // Clamp r to half the smaller side, matching SVG's <rect rx ry> behaviour
190
+ // and the canvas drawer's intent.
191
+ const radius = Math.max(0, Math.min(r, Math.min(w, h) / 2));
192
+ if (radius === 0) {
193
+ return `M${n(x)} ${n(y)} h${n(w)} v${n(h)} h${n(-w)} Z`;
194
+ }
195
+ const x2 = x + w;
196
+ const y2 = y + h;
197
+ return (`M${n(x + radius)} ${n(y)} ` +
198
+ `H${n(x2 - radius)} ` +
199
+ `A${n(radius)} ${n(radius)} 0 0 1 ${n(x2)} ${n(y + radius)} ` +
200
+ `V${n(y2 - radius)} ` +
201
+ `A${n(radius)} ${n(radius)} 0 0 1 ${n(x2 - radius)} ${n(y2)} ` +
202
+ `H${n(x + radius)} ` +
203
+ `A${n(radius)} ${n(radius)} 0 0 1 ${n(x)} ${n(y2 - radius)} ` +
204
+ `V${n(y + radius)} ` +
205
+ `A${n(radius)} ${n(radius)} 0 0 1 ${n(x + radius)} ${n(y)} Z`);
206
+ }
207
+ //# sourceMappingURL=emitter.js.map