@altano/satori-fit-text 0.1.2 → 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.
- package/README.md +1 -1
- package/dist/TextMeasurer/BrowserTextMeasurer.d.ts +1 -1
- package/dist/{BrowserTextMeasurer-TCSGQHDB.js → TextMeasurer/BrowserTextMeasurer.js} +6 -10
- package/dist/TextMeasurer/HeadlessTextMeasurer.d.ts +2 -2
- package/dist/{HeadlessTextMeasurer-46CHWL7H.js → TextMeasurer/HeadlessTextMeasurer.js} +6 -10
- package/dist/TextMeasurer/TextMeasurer.d.ts +1 -1
- package/dist/{chunk-JIONCYY3.js → TextMeasurer/TextMeasurer.js} +2 -27
- package/dist/TextMeasurer/getTextMeasurer.d.ts +1 -1
- package/dist/TextMeasurer/getTextMeasurer.js +12 -0
- package/dist/TextMeasurer/getWidthAdjustedForInlineBleed.js +17 -0
- package/dist/fitText.d.ts +1 -1
- package/dist/fitText.js +41 -0
- package/dist/index.d.ts +3 -47
- package/dist/index.js +2 -52
- package/dist/log.js +6 -0
- package/dist/types.js +0 -0
- package/package.json +26 -19
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ 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(`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",
|
|
@@ -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.js";
|
|
2
|
+
import { TextMeasurer } from "./TextMeasurer.js";
|
|
3
|
+
import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed.js";
|
|
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,5 +1,5 @@
|
|
|
1
|
-
import type { Font } from "../types";
|
|
2
|
-
import { TextMeasurer, type Dimensions } from "./TextMeasurer";
|
|
1
|
+
import type { Font } from "../types.js";
|
|
2
|
+
import { TextMeasurer, type Dimensions } from "./TextMeasurer.js";
|
|
3
3
|
export default class HeadlessTextMeasurer extends TextMeasurer {
|
|
4
4
|
#private;
|
|
5
5
|
constructor(text: string, font: Font, maxWidth: number, maxHeight: number, lineHeight: number | "normal");
|
|
@@ -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.js";
|
|
4
|
+
import { TextMeasurer } from "./TextMeasurer.js";
|
|
5
|
+
import getWidthAdjustedForInlineBleed from "./getWidthAdjustedForInlineBleed.js";
|
|
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
|
};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { TextMeasurer } from "./TextMeasurer";
|
|
1
|
+
import type { TextMeasurer } from "./TextMeasurer.js";
|
|
2
2
|
export declare function getTextMeasurer(...args: ConstructorParameters<typeof TextMeasurer>): Promise<TextMeasurer>;
|
|
3
3
|
//# sourceMappingURL=getTextMeasurer.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
async function getTextMeasurer(...args) {
|
|
2
|
+
if (typeof window !== "undefined") {
|
|
3
|
+
const Measurer = await import("./BrowserTextMeasurer.js");
|
|
4
|
+
return new Measurer.default(...args);
|
|
5
|
+
} else {
|
|
6
|
+
const Measurer = await import("./HeadlessTextMeasurer.js");
|
|
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.d.ts
CHANGED
package/dist/fitText.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getTextMeasurer } from "./TextMeasurer/getTextMeasurer.js";
|
|
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.d.ts
CHANGED
|
@@ -1,47 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type FindOptions = {
|
|
6
|
-
/**
|
|
7
|
-
* The text you want to fit
|
|
8
|
-
*/
|
|
9
|
-
text: string;
|
|
10
|
-
/**
|
|
11
|
-
* The font of your text. Options are the same as [those taken by
|
|
12
|
-
* Satori](https://github.com/vercel/satori#fonts), but only one font and not
|
|
13
|
-
* an array of fonts is accepted here.
|
|
14
|
-
*/
|
|
15
|
-
font: Font;
|
|
16
|
-
/**
|
|
17
|
-
* The max width the text must be contained in
|
|
18
|
-
*/
|
|
19
|
-
maxWidth: number;
|
|
20
|
-
/**
|
|
21
|
-
* The max height the text must be contained in
|
|
22
|
-
*/
|
|
23
|
-
maxHeight: number;
|
|
24
|
-
/**
|
|
25
|
-
* Optional: the CSS line height factor to use, e.g. 1.1
|
|
26
|
-
*/
|
|
27
|
-
lineHeight?: number | "normal";
|
|
28
|
-
/**
|
|
29
|
-
* The minimum font size that should be returned, even if smaller fonts fit
|
|
30
|
-
*/
|
|
31
|
-
minFontSize?: number;
|
|
32
|
-
/**
|
|
33
|
-
* The maximum font size that should be returned, even if larger fonts fit
|
|
34
|
-
*/
|
|
35
|
-
maxFontSize?: number;
|
|
36
|
-
/**
|
|
37
|
-
* The maximum number of font sizes to try before giving up. Defaults to 1000.
|
|
38
|
-
*/
|
|
39
|
-
maxTries?: number;
|
|
40
|
-
};
|
|
41
|
-
/**
|
|
42
|
-
* Given text, dimensions, and some options, returns the largest font size that
|
|
43
|
-
* would keep the text contained within the dimensions.
|
|
44
|
-
*/
|
|
45
|
-
declare function findLargestUsableFontSize({ text, font, maxWidth, maxHeight, lineHeight, minFontSize, maxFontSize, maxTries, }: FindOptions): Promise<number>;
|
|
46
|
-
|
|
47
|
-
export { type FindOptions, type Font, findLargestUsableFontSize };
|
|
1
|
+
export * from "./fitText.js";
|
|
2
|
+
export * from "./types.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
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.js";
|
|
2
|
+
export * from "./types.js";
|
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.4",
|
|
4
4
|
"description": "Fit text to a bounding box in Node or the browser",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -35,47 +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
38
|
"satori": "^0.10.13",
|
|
40
39
|
"satori-html": "^0.3.2",
|
|
41
40
|
"svgdom": "^0.1.19"
|
|
42
41
|
},
|
|
43
42
|
"devDependencies": {
|
|
43
|
+
"@fontsource-variable/inter": "^5.0.17",
|
|
44
|
+
"@fontsource/inter": "^5.0.17",
|
|
45
|
+
"@playwright/test": "1.40.0",
|
|
44
46
|
"@resvg/resvg-js": "^2.6.0",
|
|
45
47
|
"@types/debug": "^4.1.12",
|
|
46
48
|
"@types/he": "^1.2.3",
|
|
47
49
|
"@types/jest-image-snapshot": "^6.4.0",
|
|
50
|
+
"@types/node": "^20.11.25",
|
|
48
51
|
"@types/react": "^18.2.64",
|
|
49
52
|
"@types/react-dom": "^18.2.21",
|
|
50
53
|
"@types/svgdom": "^0.1.2",
|
|
51
|
-
"@vitest/coverage-v8": "^1.
|
|
52
|
-
"@vitest/ui": "^1.
|
|
54
|
+
"@vitest/coverage-v8": "^1.4.0",
|
|
55
|
+
"@vitest/ui": "^1.4.0",
|
|
53
56
|
"jest-image-snapshot": "^6.4.0",
|
|
54
57
|
"npm-run-all": "^4.1.5",
|
|
55
58
|
"prettier": "^3.2.5",
|
|
56
59
|
"react": "^18.2.0",
|
|
57
60
|
"react-dom": "^18.2.0",
|
|
58
|
-
"tsup": "^8.0.2",
|
|
59
61
|
"typescript": "^5.4.2",
|
|
60
|
-
"vite": "^5.
|
|
61
|
-
"vitest": "^1.
|
|
62
|
+
"vite": "^5.2.8",
|
|
63
|
+
"vitest": "^1.4.0",
|
|
62
64
|
"@altano/assets": "0.0.0",
|
|
63
|
-
"@altano/tiny-async-pool": "
|
|
65
|
+
"@altano/tiny-async-pool": "3.0.1",
|
|
64
66
|
"@altano/tsconfig": "0.0.0"
|
|
65
67
|
},
|
|
66
68
|
"scripts": {
|
|
67
|
-
"build": "tsup",
|
|
68
|
-
"build:types": "tsc --
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"lint:timing": "TIMING=1 eslint src/**/*.ts*",
|
|
72
|
-
"lint:fix": "TIMING=1 eslint src/**/*.ts* --fix",
|
|
69
|
+
"build": "tsup --config build-config/tsup.config.node.ts",
|
|
70
|
+
"build:types": "tsc --project tsconfig.declarations.json",
|
|
71
|
+
"clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
|
|
72
|
+
"dev": "pnpm run build --watch",
|
|
73
73
|
"format": "prettier --check src",
|
|
74
74
|
"format:fix": "prettier --write src",
|
|
75
|
-
"
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"test:
|
|
79
|
-
"test:
|
|
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",
|
|
85
|
+
"typecheck:src": "tsc --noEmit",
|
|
86
|
+
"typecheck:test": "tsc --noEmit --project ./tests/tsconfig.json"
|
|
80
87
|
}
|
|
81
88
|
}
|