@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 +7 -4
- package/dist/{BrowserTextMeasurer-TCSGQHDB.js → TextMeasurer/BrowserTextMeasurer.js} +6 -10
- package/dist/{HeadlessTextMeasurer-46CHWL7H.js → TextMeasurer/HeadlessTextMeasurer.js} +6 -10
- package/dist/{chunk-JIONCYY3.js → TextMeasurer/TextMeasurer.js} +2 -27
- package/dist/TextMeasurer/getTextMeasurer.js +12 -0
- package/dist/TextMeasurer/getWidthAdjustedForInlineBleed.js +17 -0
- package/dist/fitText.js +41 -0
- package/dist/index.js +2 -52
- package/dist/log.js +6 -0
- package/dist/types.js +0 -0
- package/package.json +42 -33
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
|
|
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://
|
|
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
|
-
##
|
|
44
|
+
## Example Uses
|
|
45
45
|
|
|
46
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/dist/fitText.js
ADDED
|
@@ -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
|
-
|
|
2
|
-
|
|
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
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.
|
|
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
|
-
"
|
|
39
|
-
"satori": "^0.10.3",
|
|
38
|
+
"satori": "^0.10.13",
|
|
40
39
|
"satori-html": "^0.3.2",
|
|
41
|
-
"svgdom": "^0.1.
|
|
40
|
+
"svgdom": "^0.1.19"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
47
|
-
"@
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/
|
|
50
|
-
"@types/
|
|
51
|
-
"@
|
|
52
|
-
"@
|
|
53
|
-
"
|
|
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
|
-
"
|
|
56
|
-
"react
|
|
57
|
-
"
|
|
58
|
-
"typescript": "^5.
|
|
59
|
-
"vite": "^
|
|
60
|
-
"vitest": "^
|
|
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/
|
|
63
|
-
"@altano/
|
|
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 --
|
|
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
|
-
"
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
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
|
}
|