@altano/satori-fit-text 0.1.4 → 1.0.1
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 +6 -4
- package/dist/TextMeasurer/BrowserTextMeasurer.d.ts +2 -1
- package/dist/TextMeasurer/BrowserTextMeasurer.js +7 -9
- package/dist/TextMeasurer/HeadlessTextMeasurer.d.ts +2 -1
- package/dist/TextMeasurer/HeadlessTextMeasurer.js +7 -12
- package/dist/TextMeasurer/TextMeasurer.d.ts +1 -2
- package/dist/TextMeasurer/TextMeasurer.js +0 -7
- package/dist/types.d.ts +1 -0
- package/package.json +50 -49
- package/dist/TextMeasurer/getWidthAdjustedForInlineBleed.d.ts +0 -16
- package/dist/TextMeasurer/getWidthAdjustedForInlineBleed.js +0 -17
package/README.md
CHANGED
|
@@ -21,9 +21,11 @@ pnpm add @altano/satori-fit-text
|
|
|
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(
|
|
24
|
+
const interSemiBoldBuffer = await fetch(
|
|
25
|
+
`https://cdn.jsdelivr.net/npm/@fontsource/inter@5.0.17/files/inter-latin-600-normal.woff`,
|
|
26
|
+
).then((res) => res.arrayBuffer());
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
return {
|
|
27
29
|
name: "Inter",
|
|
28
30
|
data: interSemiBoldBuffer,
|
|
29
31
|
weight: 600,
|
|
@@ -32,7 +34,7 @@ async function getInter(): Promise<Font> {
|
|
|
32
34
|
|
|
33
35
|
const largestUsableFontSize = await findLargestUsableFontSize({
|
|
34
36
|
lineHeight: 1,
|
|
35
|
-
font: await
|
|
37
|
+
font: await getInter(),
|
|
36
38
|
text: "Some text I want to be as big as possible",
|
|
37
39
|
maxWidth: 1136,
|
|
38
40
|
maxHeight: 429,
|
|
@@ -46,7 +48,7 @@ console.log(largestUsableFontSize);
|
|
|
46
48
|
- [React Component (sandbox)](https://codesandbox.io/s/altano-satori-fit-text-browser-example-j2gvk6)
|
|
47
49
|
- [Next.js Open Graph Card - Simple (codesandbox)](https://codesandbox.io/p/sandbox/altano-satori-fit-text-next-js-demo-sv4flc)
|
|
48
50
|
- [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/
|
|
51
|
+
- [Node.js CLI App (codesandbox)](https://codesandbox.io/p/sandbox/github/altano/npm-packages/tree/main/examples/satori-fit-text-node-cli?file=%2Fpackage.json%3A1%2C1)
|
|
50
52
|
|
|
51
53
|
## Implementation
|
|
52
54
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { TextMeasurer
|
|
1
|
+
import { TextMeasurer } from "./TextMeasurer.js";
|
|
2
|
+
import type { Dimensions } from "../types.js";
|
|
2
3
|
export default class BrowserTextMeasurer extends TextMeasurer {
|
|
3
4
|
#private;
|
|
4
5
|
getDimensions(fontSize: number): Promise<Dimensions>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import log from "../log.js";
|
|
2
2
|
import { TextMeasurer } from "./TextMeasurer.js";
|
|
3
|
-
import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed.js";
|
|
4
3
|
class BrowserTextMeasurer extends TextMeasurer {
|
|
5
4
|
#getSvgElement(svgText) {
|
|
6
5
|
const div = document.createElement("div");
|
|
@@ -17,17 +16,16 @@ class BrowserTextMeasurer extends TextMeasurer {
|
|
|
17
16
|
const svg = this.#getSvgElement(svgText);
|
|
18
17
|
try {
|
|
19
18
|
document.body.appendChild(svg);
|
|
20
|
-
const
|
|
21
|
-
const
|
|
19
|
+
const dimensions = svg.getBBox();
|
|
20
|
+
const { width, height } = dimensions;
|
|
22
21
|
log({
|
|
23
22
|
fontSize,
|
|
24
|
-
width
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
heightFits: height <= this.maxHeight
|
|
23
|
+
width: `${width}px`,
|
|
24
|
+
height: `${height}px`,
|
|
25
|
+
widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
|
|
26
|
+
heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
|
|
29
27
|
});
|
|
30
|
-
return
|
|
28
|
+
return dimensions;
|
|
31
29
|
} finally {
|
|
32
30
|
svg.parentElement?.removeChild?.(svg);
|
|
33
31
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Font } from "../types.js";
|
|
2
|
-
import { TextMeasurer
|
|
2
|
+
import { TextMeasurer } from "./TextMeasurer.js";
|
|
3
|
+
import { Dimensions } from "../types.js";
|
|
3
4
|
export default class HeadlessTextMeasurer extends TextMeasurer {
|
|
4
5
|
#private;
|
|
5
6
|
constructor(text: string, font: Font, maxWidth: number, maxHeight: number, lineHeight: number | "normal");
|
|
@@ -2,7 +2,6 @@ import { SVG, registerWindow } from "@svgdotjs/svg.js";
|
|
|
2
2
|
import { createSVGWindow } from "svgdom";
|
|
3
3
|
import log from "../log.js";
|
|
4
4
|
import { TextMeasurer } from "./TextMeasurer.js";
|
|
5
|
-
import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed.js";
|
|
6
5
|
class HeadlessTextMeasurer extends TextMeasurer {
|
|
7
6
|
#canvas;
|
|
8
7
|
constructor(text, font, maxWidth, maxHeight, lineHeight) {
|
|
@@ -14,20 +13,16 @@ class HeadlessTextMeasurer extends TextMeasurer {
|
|
|
14
13
|
}
|
|
15
14
|
async getDimensions(fontSize) {
|
|
16
15
|
await this.#setFontSize(fontSize);
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
this.#canvas.node,
|
|
20
|
-
width
|
|
21
|
-
);
|
|
16
|
+
const dimensions = this.#canvas.bbox();
|
|
17
|
+
const { width, height } = dimensions;
|
|
22
18
|
log({
|
|
23
19
|
fontSize,
|
|
24
|
-
width
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
heightFits: height <= this.maxHeight
|
|
20
|
+
width: `${width}px`,
|
|
21
|
+
height: `${height}px`,
|
|
22
|
+
widthFits: `${width}px ${width <= this.maxWidth ? "DOES" : "does NOT"} fit in ${this.maxWidth}`,
|
|
23
|
+
heightFits: `${height}px ${height <= this.maxHeight ? "DOES" : "does NOT"} fit in ${this.maxHeight}`
|
|
29
24
|
});
|
|
30
|
-
return
|
|
25
|
+
return dimensions;
|
|
31
26
|
}
|
|
32
27
|
async #setFontSize(fontSize) {
|
|
33
28
|
const svgText = await this.generateSvg(fontSize);
|
|
@@ -35,13 +35,6 @@ class TextMeasurer {
|
|
|
35
35
|
margin: 0,
|
|
36
36
|
padding: 0,
|
|
37
37
|
boxSizing: "border-box",
|
|
38
|
-
// We render a border so that we measure the true dimensions of the text.
|
|
39
|
-
// Without one, the bounding box might not include significant whitespace.
|
|
40
|
-
// For example, if the last line of text has no descenders, the bounding box
|
|
41
|
-
// would only extend to the baseline of the last line. With a border, the
|
|
42
|
-
// bounding box will include all the whitespace between the baseline and the
|
|
43
|
-
// descent of the text.
|
|
44
|
-
border: "solid red 1px",
|
|
45
38
|
// Dynamic styles based on input
|
|
46
39
|
fontSize,
|
|
47
40
|
fontFamily: this.font.name,
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,88 +1,89 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altano/satori-fit-text",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Fit text to a bounding box in Node or the browser",
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"url": "https://alan.norbauer.com"
|
|
13
|
-
},
|
|
14
|
-
"license": "ISC",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fit-text",
|
|
7
|
+
"opengraph",
|
|
8
|
+
"satori",
|
|
9
|
+
"typography"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/altano/npm-packages/tree/main/packages/satori-fit-text",
|
|
15
12
|
"repository": {
|
|
16
13
|
"type": "git",
|
|
17
14
|
"url": "https://github.com/altano/npm-packages",
|
|
18
15
|
"directory": "packages/satori-fit-text"
|
|
19
16
|
},
|
|
20
|
-
"
|
|
17
|
+
"license": "ISC",
|
|
18
|
+
"author": {
|
|
19
|
+
"name": "Alan Norbauer",
|
|
20
|
+
"email": "altano@gmail.com",
|
|
21
|
+
"url": "https://alan.norbauer.com"
|
|
22
|
+
},
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"type": "module",
|
|
25
|
+
"exports": "./dist/index.js",
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
21
27
|
"files": [
|
|
22
28
|
"assets/",
|
|
23
29
|
"dist/**/*.js",
|
|
24
30
|
"dist/**/*.d.ts"
|
|
25
31
|
],
|
|
26
|
-
"publishConfig": {
|
|
27
|
-
"access": "public"
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"typography",
|
|
31
|
-
"satori",
|
|
32
|
-
"opengraph",
|
|
33
|
-
"fit-text"
|
|
34
|
-
],
|
|
35
32
|
"dependencies": {
|
|
36
|
-
"@svgdotjs/svg.js": "^3.2.
|
|
37
|
-
"debug": "^4.
|
|
38
|
-
"satori": "^0.
|
|
39
|
-
"
|
|
40
|
-
"svgdom": "^0.1.19"
|
|
33
|
+
"@svgdotjs/svg.js": "^3.2.4",
|
|
34
|
+
"debug": "^4.4.0",
|
|
35
|
+
"satori": "^0.12.2",
|
|
36
|
+
"svgdom": "^0.1.21"
|
|
41
37
|
},
|
|
42
38
|
"devDependencies": {
|
|
43
|
-
"@
|
|
44
|
-
"@fontsource/inter": "^5.
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
39
|
+
"@arethetypeswrong/cli": "^0.17.4",
|
|
40
|
+
"@fontsource-variable/inter": "^5.2.5",
|
|
41
|
+
"@fontsource/inter": "^5.2.5",
|
|
42
|
+
"@playwright/test": "1.47.0",
|
|
43
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
47
44
|
"@types/debug": "^4.1.12",
|
|
48
45
|
"@types/he": "^1.2.3",
|
|
49
46
|
"@types/jest-image-snapshot": "^6.4.0",
|
|
50
|
-
"@types/node": "^
|
|
51
|
-
"@types/react": "^
|
|
52
|
-
"@types/react-dom": "^
|
|
47
|
+
"@types/node": "^22.13.10",
|
|
48
|
+
"@types/react": "^19.0.12",
|
|
49
|
+
"@types/react-dom": "^19.0.4",
|
|
53
50
|
"@types/svgdom": "^0.1.2",
|
|
54
|
-
"@vitest/coverage-v8": "^1.
|
|
55
|
-
"
|
|
51
|
+
"@vitest/coverage-v8": "^3.1.1",
|
|
52
|
+
"depcheck": "^1.4.7",
|
|
56
53
|
"jest-image-snapshot": "^6.4.0",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"react": "^
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"vitest": "^1.4.0",
|
|
54
|
+
"prettier": "^3.5.3",
|
|
55
|
+
"react": "^19.1.0",
|
|
56
|
+
"react-dom": "^19.1.0",
|
|
57
|
+
"typescript": "^5.8.2",
|
|
58
|
+
"vite": "^6.2.4",
|
|
59
|
+
"vitest": "^3.1.1",
|
|
64
60
|
"@altano/assets": "0.0.0",
|
|
65
|
-
"@altano/
|
|
61
|
+
"@altano/build-config": "0.0.0",
|
|
62
|
+
"@altano/testing": "0.0.0",
|
|
66
63
|
"@altano/tsconfig": "0.0.0"
|
|
67
64
|
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsup --config build-config/tsup.config.node.ts",
|
|
70
70
|
"build:types": "tsc --project tsconfig.declarations.json",
|
|
71
|
+
"check:exports": "attw --pack . --profile esm-only",
|
|
72
|
+
"check:types:src": "tsc --noEmit",
|
|
73
|
+
"check:types:test": "tsc --noEmit --project ./tests/tsconfig.json",
|
|
74
|
+
"check:unused-dependencies": "depcheck",
|
|
71
75
|
"clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
|
|
72
76
|
"dev": "pnpm run build --watch",
|
|
73
77
|
"format": "prettier --check src",
|
|
74
78
|
"format:fix": "prettier --write src",
|
|
75
|
-
"lint": "eslint
|
|
79
|
+
"lint": "eslint",
|
|
76
80
|
"lint:fix": "TIMING=1 pnpm lint --fix",
|
|
77
81
|
"lint:timing": "TIMING=1 pnpm lint",
|
|
78
82
|
"test:e2e": "playwright test",
|
|
79
83
|
"test:e2e:server": "vite --strictPort --port 3717 ./tests/e2e",
|
|
80
84
|
"test:e2e:ui": "playwright test --ui",
|
|
81
|
-
"test:unit": "
|
|
85
|
+
"test:unit": "vitest --run",
|
|
82
86
|
"test:unit:benchmark": "vitest --run bench",
|
|
83
|
-
"test:unit:
|
|
84
|
-
"test:unit:watch": "vitest",
|
|
85
|
-
"typecheck:src": "tsc --noEmit",
|
|
86
|
-
"typecheck:test": "tsc --noEmit --project ./tests/tsconfig.json"
|
|
87
|
+
"test:unit:watch": "vitest"
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Satori will sometimes render text slightly outside the div on the left (e.g.
|
|
3
|
-
* x:-2). Having text slightly outside the box and drawing a border means we
|
|
4
|
-
* will have a bounding box on the svg that is slightly larger than the actual
|
|
5
|
-
* text size (by the amount of bleed).
|
|
6
|
-
*
|
|
7
|
-
* For example, if text starts at -2 and the div's bounding box is x:0 to
|
|
8
|
-
* x:1200, the svg's bounding box will have width=1202px. And that won't
|
|
9
|
-
* represent the actual width of the text.
|
|
10
|
-
*
|
|
11
|
-
* Let's account for this by just reducing the bounding box width by the amount
|
|
12
|
-
* the text is bleeding outside the left or right and trust that Satori isn't
|
|
13
|
-
* both letting the text bleed AND filling too much space.
|
|
14
|
-
*/
|
|
15
|
-
export default function getWidthAdjustedForInlineBleed(svg: SVGSVGElement, svgWidth: number): number;
|
|
16
|
-
//# sourceMappingURL=getWidthAdjustedForInlineBleed.d.ts.map
|
|
@@ -1,17 +0,0 @@
|
|
|
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
|
-
};
|