@altano/satori-fit-text 0.1.1 → 0.1.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/README.md CHANGED
@@ -15,13 +15,13 @@ yarn add @altano/satori-fit-text
15
15
  pnpm add @altano/satori-fit-text
16
16
  ```
17
17
 
18
- ## Example Usage
18
+ ## Basic Example
19
19
 
20
20
  ```ts
21
21
  import { findLargestUsableFontSize, type Font } from "@altano/satori-fit-text";
22
22
 
23
23
  async function getInter(): Promise<Font> {
24
- const interSemiBoldBuffer = await fetch(`https://rsms.me/inter/font-files/Inter-SemiBold.woff`).then((res) => res.arrayBuffer());
24
+ const interSemiBoldBuffer = await fetch(`https://cdn.jsdelivr.net/npm/@fontsource/inter@5.0.17/files/inter-latin-600-normal.woff`).then((res) => res.arrayBuffer());
25
25
 
26
26
  const font = {
27
27
  name: "Inter",
@@ -41,9 +41,12 @@ const largestUsableFontSize = await findLargestUsableFontSize({
41
41
  console.log(largestUsableFontSize);
42
42
  ```
43
43
 
44
- ## Potential Uses
44
+ ## Example Uses
45
45
 
46
- Because this measures the maximum font size using Satori, the ideal use of this library is to determine the largest possible font size in something like an [Open Graph card](https://ogp.me/). If you dynamically generate Open Graph cards with text (e.g. the title of your blog post) you may want the text to fill the card's area as best as possible without overflowing. This library can help you achieve that.
46
+ - [React Component (sandbox)](https://codesandbox.io/s/altano-satori-fit-text-browser-example-j2gvk6)
47
+ - [Next.js Open Graph Card - Simple (codesandbox)](https://codesandbox.io/p/sandbox/altano-satori-fit-text-next-js-demo-sv4flc)
48
+ - [Next.js Open Graph Card - Complex](https://github.com/altano/alan.norbauer.com/blob/main/src/components/opengraph/image.tsx#L29-L35)
49
+ - [Node.js CLI App (codesandbox)](https://codesandbox.io/p/sandbox/github/altano/npm-packages/tree/main/codesandboxes/satori-fit-text-node-cli?file=%2Fpackage.json%3A1%2C1)
47
50
 
48
51
  ## Implementation
49
52
 
@@ -1,11 +1,7 @@
1
- import {
2
- TextMeasurer,
3
- getWidthAdjustedForInlineBleed,
4
- log_default
5
- } from "./chunk-JIONCYY3.js";
6
-
7
- // src/TextMeasurer/BrowserTextMeasurer.ts
8
- var BrowserTextMeasurer = class extends TextMeasurer {
1
+ import log from "../log";
2
+ import { TextMeasurer } from "./TextMeasurer";
3
+ import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed";
4
+ class BrowserTextMeasurer extends TextMeasurer {
9
5
  #getSvgElement(svgText) {
10
6
  const div = document.createElement("div");
11
7
  div.style.position = "fixed";
@@ -23,7 +19,7 @@ var BrowserTextMeasurer = class extends TextMeasurer {
23
19
  document.body.appendChild(svg);
24
20
  const { width, height } = svg.getBBox();
25
21
  const widthAdjusted = getWidthAdjustedForInlineBleed(svg, width);
26
- log_default({
22
+ log({
27
23
  fontSize,
28
24
  width,
29
25
  widthAdjusted,
@@ -36,7 +32,7 @@ var BrowserTextMeasurer = class extends TextMeasurer {
36
32
  svg.parentElement?.removeChild?.(svg);
37
33
  }
38
34
  }
39
- };
35
+ }
40
36
  export {
41
37
  BrowserTextMeasurer as default
42
38
  };
@@ -1,13 +1,9 @@
1
- import {
2
- TextMeasurer,
3
- getWidthAdjustedForInlineBleed,
4
- log_default
5
- } from "./chunk-JIONCYY3.js";
6
-
7
- // src/TextMeasurer/HeadlessTextMeasurer.ts
8
1
  import { SVG, registerWindow } from "@svgdotjs/svg.js";
9
2
  import { createSVGWindow } from "svgdom";
10
- var HeadlessTextMeasurer = class extends TextMeasurer {
3
+ import log from "../log";
4
+ import { TextMeasurer } from "./TextMeasurer";
5
+ import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed";
6
+ class HeadlessTextMeasurer extends TextMeasurer {
11
7
  #canvas;
12
8
  constructor(text, font, maxWidth, maxHeight, lineHeight) {
13
9
  super(text, font, maxWidth, maxHeight, lineHeight);
@@ -23,7 +19,7 @@ var HeadlessTextMeasurer = class extends TextMeasurer {
23
19
  this.#canvas.node,
24
20
  width
25
21
  );
26
- log_default({
22
+ log({
27
23
  fontSize,
28
24
  width,
29
25
  widthAdjusted,
@@ -38,7 +34,7 @@ var HeadlessTextMeasurer = class extends TextMeasurer {
38
34
  this.#canvas.clear();
39
35
  this.#canvas.svg(svgText);
40
36
  }
41
- };
37
+ }
42
38
  export {
43
39
  HeadlessTextMeasurer as default
44
40
  };
@@ -1,11 +1,5 @@
1
- // src/log.ts
2
- import debug from "debug";
3
- var log = debug("satori-text-fit");
4
- var log_default = log;
5
-
6
- // src/TextMeasurer/TextMeasurer.ts
7
1
  import satori from "satori";
8
- var TextMeasurer = class {
2
+ class TextMeasurer {
9
3
  constructor(text, font, maxWidth, maxHeight, lineHeight) {
10
4
  this.text = text;
11
5
  this.font = font;
@@ -66,26 +60,7 @@ var TextMeasurer = class {
66
60
  height: this.maxHeight * 2
67
61
  });
68
62
  }
69
- };
70
-
71
- // src/TextMeasurer/getWidthAdjustedForInlineBleed.ts
72
- function getWidthAdjustedForInlineBleed(svg, svgWidth) {
73
- let smallestXValue = 0;
74
- let largestYValue = svgWidth;
75
- for (const child of svg.children) {
76
- if ("getBBox" in child && typeof child["getBBox"] === "function") {
77
- const { x, y } = child.getBBox();
78
- smallestXValue = Math.min(smallestXValue, x);
79
- largestYValue = Math.max(largestYValue, y);
80
- }
81
- }
82
- const xAdjustment = Math.ceil(Math.abs(smallestXValue));
83
- const yAdjustment = largestYValue <= svgWidth ? 0 : Math.ceil(largestYValue) - svgWidth;
84
- return svgWidth - xAdjustment - yAdjustment;
85
63
  }
86
-
87
64
  export {
88
- log_default,
89
- TextMeasurer,
90
- getWidthAdjustedForInlineBleed
65
+ TextMeasurer
91
66
  };
@@ -0,0 +1,12 @@
1
+ async function getTextMeasurer(...args) {
2
+ if (typeof window !== "undefined") {
3
+ const Measurer = await import("./BrowserTextMeasurer");
4
+ return new Measurer.default(...args);
5
+ } else {
6
+ const Measurer = await import("./HeadlessTextMeasurer");
7
+ return new Measurer.default(...args);
8
+ }
9
+ }
10
+ export {
11
+ getTextMeasurer
12
+ };
@@ -0,0 +1,17 @@
1
+ function getWidthAdjustedForInlineBleed(svg, svgWidth) {
2
+ let smallestXValue = 0;
3
+ let largestYValue = svgWidth;
4
+ for (const child of svg.children) {
5
+ if ("getBBox" in child && typeof child["getBBox"] === "function") {
6
+ const { x, y } = child.getBBox();
7
+ smallestXValue = Math.min(smallestXValue, x);
8
+ largestYValue = Math.max(largestYValue, y);
9
+ }
10
+ }
11
+ const xAdjustment = Math.ceil(Math.abs(smallestXValue));
12
+ const yAdjustment = largestYValue <= svgWidth ? 0 : Math.ceil(largestYValue) - svgWidth;
13
+ return svgWidth - xAdjustment - yAdjustment;
14
+ }
15
+ export {
16
+ getWidthAdjustedForInlineBleed as default
17
+ };
@@ -0,0 +1,41 @@
1
+ import { getTextMeasurer } from "./TextMeasurer/getTextMeasurer";
2
+ async function findLargestUsableFontSize({
3
+ text,
4
+ font,
5
+ maxWidth,
6
+ maxHeight,
7
+ lineHeight = "normal",
8
+ minFontSize = 1,
9
+ maxFontSize = 1e3,
10
+ maxTries = 1e3
11
+ }) {
12
+ if (maxTries < 2) {
13
+ throw new Error("`maxTries` must be >= 2");
14
+ }
15
+ const textMeasurer = await getTextMeasurer(
16
+ text,
17
+ font,
18
+ maxWidth,
19
+ maxHeight,
20
+ lineHeight
21
+ );
22
+ for (let tries = 1, lower = minFontSize, upper = maxFontSize; tries < maxTries; ++tries) {
23
+ const searchCutoffThreshold = Math.min(1, lower);
24
+ if (upper - lower <= searchCutoffThreshold) {
25
+ return Math.floor(lower);
26
+ }
27
+ const sizeCandidate = Math.floor(lower + (upper - lower) / 2);
28
+ const doesFit = await textMeasurer.doesSizeFit(sizeCandidate);
29
+ if (doesFit) {
30
+ lower = sizeCandidate;
31
+ } else {
32
+ upper = sizeCandidate;
33
+ }
34
+ }
35
+ throw new Error(
36
+ `Could not find a valid font size in ${maxTries - 1} attempts. Giving up. Please try again after constraining the search space, e.g. by restricting the min/max font size difference.`
37
+ );
38
+ }
39
+ export {
40
+ findLargestUsableFontSize
41
+ };
package/dist/index.js CHANGED
@@ -1,52 +1,2 @@
1
- // src/TextMeasurer/getTextMeasurer.ts
2
- async function getTextMeasurer(...args) {
3
- if (typeof window !== "undefined") {
4
- const Measurer = await import("./BrowserTextMeasurer-TCSGQHDB.js");
5
- return new Measurer.default(...args);
6
- } else {
7
- const Measurer = await import("./HeadlessTextMeasurer-46CHWL7H.js");
8
- return new Measurer.default(...args);
9
- }
10
- }
11
-
12
- // src/fitText.ts
13
- async function findLargestUsableFontSize({
14
- text,
15
- font,
16
- maxWidth,
17
- maxHeight,
18
- lineHeight = "normal",
19
- minFontSize = 1,
20
- maxFontSize = 1e3,
21
- maxTries = 1e3
22
- }) {
23
- if (maxTries < 2) {
24
- throw new Error("`maxTries` must be >= 2");
25
- }
26
- const textMeasurer = await getTextMeasurer(
27
- text,
28
- font,
29
- maxWidth,
30
- maxHeight,
31
- lineHeight
32
- );
33
- for (let tries = 1, lower = minFontSize, upper = maxFontSize; tries < maxTries; ++tries) {
34
- const searchCutoffThreshold = Math.min(1, lower);
35
- if (upper - lower <= searchCutoffThreshold) {
36
- return Math.floor(lower);
37
- }
38
- const sizeCandidate = Math.floor(lower + (upper - lower) / 2);
39
- const doesFit = await textMeasurer.doesSizeFit(sizeCandidate);
40
- if (doesFit) {
41
- lower = sizeCandidate;
42
- } else {
43
- upper = sizeCandidate;
44
- }
45
- }
46
- throw new Error(
47
- `Could not find a valid font size in ${maxTries - 1} attempts. Giving up. Please try again after constraining the search space, e.g. by restricting the min/max font size difference.`
48
- );
49
- }
50
- export {
51
- findLargestUsableFontSize
52
- };
1
+ export * from "./fitText";
2
+ export * from "./types";
package/dist/log.js ADDED
@@ -0,0 +1,6 @@
1
+ import debug from "debug";
2
+ const log = debug("satori-text-fit");
3
+ var log_default = log;
4
+ export {
5
+ log_default as default
6
+ };
package/dist/types.js ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altano/satori-fit-text",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Fit text to a bounding box in Node or the browser",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -35,45 +35,54 @@
35
35
  "dependencies": {
36
36
  "@svgdotjs/svg.js": "^3.2.0",
37
37
  "debug": "^4.3.4",
38
- "he": "^1.2.0",
39
- "satori": "^0.10.3",
38
+ "satori": "^0.10.13",
40
39
  "satori-html": "^0.3.2",
41
- "svgdom": "^0.1.14"
40
+ "svgdom": "^0.1.19"
42
41
  },
43
42
  "devDependencies": {
44
- "@resvg/resvg-js": "^2.4.1",
45
- "@types/debug": "^4.1.8",
46
- "@types/he": "^1.2.0",
47
- "@types/jest-image-snapshot": "^6.2.0",
48
- "@types/react": "^18.2.21",
49
- "@types/react-dom": "^18.2.7",
50
- "@types/svgdom": "^0.1.0",
51
- "@vitest/coverage-v8": "^0.34.3",
52
- "@vitest/ui": "^0.30.1",
53
- "jest-image-snapshot": "^6.2.0",
43
+ "@fontsource-variable/inter": "^5.0.17",
44
+ "@fontsource/inter": "^5.0.17",
45
+ "@playwright/test": "1.40.0",
46
+ "@resvg/resvg-js": "^2.6.0",
47
+ "@types/debug": "^4.1.12",
48
+ "@types/he": "^1.2.3",
49
+ "@types/jest-image-snapshot": "^6.4.0",
50
+ "@types/node": "^20.11.25",
51
+ "@types/react": "^18.2.64",
52
+ "@types/react-dom": "^18.2.21",
53
+ "@types/svgdom": "^0.1.2",
54
+ "@vitest/coverage-v8": "^1.4.0",
55
+ "@vitest/ui": "^1.4.0",
56
+ "jest-image-snapshot": "^6.4.0",
54
57
  "npm-run-all": "^4.1.5",
55
- "react": "^18",
56
- "react-dom": "^18",
57
- "tsup": "^7.2.0",
58
- "typescript": "^5.1.6",
59
- "vite": "^4.4.9",
60
- "vitest": "^0.34.3",
58
+ "prettier": "^3.2.5",
59
+ "react": "^18.2.0",
60
+ "react-dom": "^18.2.0",
61
+ "typescript": "^5.4.2",
62
+ "vite": "^5.2.4",
63
+ "vitest": "^1.4.0",
61
64
  "@altano/assets": "0.0.0",
62
- "@altano/tsconfig": "0.0.0",
63
- "@altano/tiny-async-pool": "1.0.2"
65
+ "@altano/tiny-async-pool": "3.0.0",
66
+ "@altano/tsconfig": "0.0.0"
64
67
  },
65
68
  "scripts": {
66
- "build": "tsup",
67
- "build:types": "tsc --emitDeclarationOnly --declaration",
68
- "dev": "tsup --watch",
69
- "lint": "eslint src/**/*.ts*",
70
- "lint:timing": "TIMING=1 eslint src/**/*.ts*",
71
- "lint:fix": "TIMING=1 eslint src/**/*.ts* --fix",
72
- "format": "prettier --write src",
69
+ "build": "tsup --config build-config/tsup.config.node.ts",
70
+ "build:types": "tsc --project tsconfig.declarations.json",
73
71
  "clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
74
- "test": "run-s test:unit test:benchmark",
75
- "test:unit": "vitest --run",
76
- "test:benchmark": "vitest --run bench",
77
- "test:watch": "vitest tests/"
72
+ "dev": "pnpm run build --watch",
73
+ "format": "prettier --check src",
74
+ "format:fix": "prettier --write src",
75
+ "lint": "eslint package.json src/**/*.ts*",
76
+ "lint:fix": "TIMING=1 pnpm lint --fix",
77
+ "lint:timing": "TIMING=1 pnpm lint",
78
+ "test:e2e": "playwright test",
79
+ "test:e2e:server": "vite --strictPort --port 3717 ./tests/e2e",
80
+ "test:e2e:ui": "playwright test --ui",
81
+ "test:unit": "run-s test:unit:run test:unit:benchmark",
82
+ "test:unit:benchmark": "vitest --run bench",
83
+ "test:unit:run": "vitest --run",
84
+ "test:unit:watch": "vitest tests/unit/",
85
+ "typecheck:src": "tsc --noEmit",
86
+ "typecheck:test": "tsc --noEmit --project ./tests/tsconfig.json"
78
87
  }
79
88
  }