@akairos/typeset-studio 0.2.8 → 0.3.0
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 +20 -0
- package/dist/assets/{index-Dn5YtK3W.js → index-DMxjiZmE.js} +17 -17
- package/dist/cover/cover.js +120 -0
- package/dist/editor/editor.js +1 -0
- package/dist/index.html +1 -1
- package/dist/lib/adapter.js +1 -1
- package/dist/lib/{canvas-renderer-wce6xgb2.js → canvas-renderer-hpe2bqkt.js} +3 -2
- package/dist/lib/{font-loader-j28mt43t.js → font-loader-cft81crk.js} +2 -1
- package/dist/lib/{index-qszn98j2.js → index-1yt7sbqe.js} +12 -2
- package/dist/lib/index-2vgqm8ms.js +167 -0
- package/dist/lib/{index-697fkv5y.js → index-dz9pmgbk.js} +12 -2
- package/dist/lib/{index-y2tk7642.js → index-q952vprv.js} +4 -4
- package/dist/lib/index-r4swepwa.js +47 -0
- package/dist/lib/{index-6c43c7h4.js → index-xg6pyfw6.js} +1 -163
- package/dist/lib/index.js +6 -4
- package/dist/lib/render-fvtcq3hb.js +13 -0
- package/dist/lib/server-font.js +8 -0
- package/package.json +16 -2
- package/src/adapter.ts +1 -1
- package/src/core/article-cover.test.ts +39 -0
- package/src/core/article-cover.ts +92 -0
- package/src/core/canvas-draw.ts +9 -1
- package/src/core/types.ts +1 -0
- package/src/cover.ts +9 -0
- package/src/server/index.ts +1 -1
- package/src/server/routes/render.ts +9 -0
- package/src/server-font.test.ts +46 -0
- package/src/server-font.ts +23 -0
- package/src/web/components/CoverControls.tsx +22 -0
- package/src/web/components/Header.tsx +1 -1
- package/src/web/editor.ts +1 -0
- package/dist/lib/render-064weavx.js +0 -11
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// src/core/article-cover.ts
|
|
2
|
+
var ARTICLE_COVER_FONT = Object.freeze({
|
|
3
|
+
id: "publishing-noto-cjk-regular-v1",
|
|
4
|
+
sha256: "b76b0433203017ca80401b2ee0dd69350349871c4b19d504c34dbdd80541690a",
|
|
5
|
+
family: "Publishing Cover"
|
|
6
|
+
});
|
|
7
|
+
var ARTICLE_COVER_IMAGE_KEY = "article_cover_image";
|
|
8
|
+
var DEFAULT_ARTICLE_COVER_CONTROLS = Object.freeze({
|
|
9
|
+
backgroundColor: "#ffffff",
|
|
10
|
+
titleColor: "#171717",
|
|
11
|
+
fontSize: 120,
|
|
12
|
+
textAlign: "left",
|
|
13
|
+
safePadding: 120
|
|
14
|
+
});
|
|
15
|
+
function object(value) {
|
|
16
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
17
|
+
throw new RangeError("Invalid article cover object");
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function exactKeys(value, keys) {
|
|
21
|
+
if (Object.keys(value).length !== keys.length || Object.keys(value).some((key) => !keys.includes(key)))
|
|
22
|
+
throw new RangeError("Unsupported article cover fields");
|
|
23
|
+
}
|
|
24
|
+
function color(value) {
|
|
25
|
+
if (typeof value !== "string" || !/^#[a-f\d]{6}$/i.test(value))
|
|
26
|
+
throw new RangeError("Article cover colors must be six-digit hex");
|
|
27
|
+
return value.toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
function validateArticleCoverControls(value) {
|
|
30
|
+
const input = object(value);
|
|
31
|
+
exactKeys(input, ["backgroundColor", "titleColor", "fontSize", "textAlign", "safePadding"]);
|
|
32
|
+
if (!Number.isInteger(input.fontSize) || input.fontSize < 80 || input.fontSize > 200)
|
|
33
|
+
throw new RangeError("Article cover fontSize must be an integer between 80 and 200");
|
|
34
|
+
if (!Number.isInteger(input.safePadding) || input.safePadding < 60 || input.safePadding > 200)
|
|
35
|
+
throw new RangeError("Article cover safePadding must be an integer between 60 and 200");
|
|
36
|
+
if (!["left", "center", "right"].includes(input.textAlign))
|
|
37
|
+
throw new RangeError("Invalid article cover textAlign");
|
|
38
|
+
return {
|
|
39
|
+
backgroundColor: color(input.backgroundColor),
|
|
40
|
+
titleColor: color(input.titleColor),
|
|
41
|
+
fontSize: input.fontSize,
|
|
42
|
+
textAlign: input.textAlign,
|
|
43
|
+
safePadding: input.safePadding
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createArticleCoverConfig(value) {
|
|
47
|
+
const input = object(value);
|
|
48
|
+
exactKeys(input, ["id", "name", "controls", "assetId"]);
|
|
49
|
+
const uuid = /^[a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}$/i;
|
|
50
|
+
if (typeof input.id !== "string" || !uuid.test(input.id) || typeof input.assetId !== "string" || !uuid.test(input.assetId))
|
|
51
|
+
throw new RangeError("Article cover and asset ids must be UUIDs");
|
|
52
|
+
if (typeof input.name !== "string" || !input.name.trim() || input.name.trim().length > 80)
|
|
53
|
+
throw new RangeError("Article cover name must contain 1 to 80 characters");
|
|
54
|
+
const controls = validateArticleCoverControls(input.controls);
|
|
55
|
+
const safeArea = { top: controls.safePadding, right: 1000 + controls.safePadding, bottom: controls.safePadding, left: controls.safePadding };
|
|
56
|
+
const font = { fontFamily: ARTICLE_COVER_FONT.family, fontSize: controls.fontSize, minFontSize: controls.fontSize, fontWeight: 400, lineHeight: 1.2, letterSpacing: 0 };
|
|
57
|
+
return {
|
|
58
|
+
id: input.id,
|
|
59
|
+
name: input.name.trim(),
|
|
60
|
+
slot: "article_cover",
|
|
61
|
+
aspectRatio: "wechat-cover-2.35",
|
|
62
|
+
algorithm: {
|
|
63
|
+
baseFontSize: controls.fontSize,
|
|
64
|
+
scaleRatio: 1,
|
|
65
|
+
lineHeightMultiplier: 1.2,
|
|
66
|
+
obstacleGap: 0,
|
|
67
|
+
targetFillRatio: 1,
|
|
68
|
+
contentBottomGap: 0,
|
|
69
|
+
textAlign: controls.textAlign,
|
|
70
|
+
verticalAlign: "center",
|
|
71
|
+
safeArea
|
|
72
|
+
},
|
|
73
|
+
theme: {
|
|
74
|
+
bgColor: controls.backgroundColor,
|
|
75
|
+
textColor: controls.titleColor,
|
|
76
|
+
headingColor: controls.titleColor,
|
|
77
|
+
accentColor: controls.titleColor,
|
|
78
|
+
quoteColor: controls.titleColor,
|
|
79
|
+
captionColor: controls.titleColor,
|
|
80
|
+
watermarkColor: controls.titleColor,
|
|
81
|
+
cardBg: controls.backgroundColor
|
|
82
|
+
},
|
|
83
|
+
typography: { headingFont: ARTICLE_COVER_FONT.family, bodyFont: ARTICLE_COVER_FONT.family },
|
|
84
|
+
footerText: "",
|
|
85
|
+
watermarkText: "",
|
|
86
|
+
chrome: { showHeader: false, showWatermark: false, showPageNumber: false, showFooter: false, showFooterDivider: false, showFooterText: false, showFillRatio: false },
|
|
87
|
+
coverTheme: {
|
|
88
|
+
id: input.id,
|
|
89
|
+
format: "wechat-cover-split",
|
|
90
|
+
typography: { title: { ...font }, subtitle: { ...font }, footer: { ...font } },
|
|
91
|
+
colors: { text: controls.titleColor, accent: controls.titleColor, muted: controls.titleColor },
|
|
92
|
+
background: { fill: { type: "solid", color: controls.backgroundColor }, image: null, overlay: null },
|
|
93
|
+
decoration: { pattern: "none", color: controls.titleColor, opacity: 0, size: 1, image: null },
|
|
94
|
+
layout: { textAlign: controls.textAlign, verticalAlign: "center", safeArea: { ...safeArea } }
|
|
95
|
+
},
|
|
96
|
+
images: [{
|
|
97
|
+
id: ARTICLE_COVER_IMAGE_KEY,
|
|
98
|
+
templateAssetKey: ARTICLE_COVER_IMAGE_KEY,
|
|
99
|
+
src: `asset:${input.assetId}`,
|
|
100
|
+
alt: "文章封面方图",
|
|
101
|
+
fit: "cover",
|
|
102
|
+
isAbsolute: true,
|
|
103
|
+
x: 2350 - controls.safePadding,
|
|
104
|
+
y: -controls.safePadding,
|
|
105
|
+
width: 1000,
|
|
106
|
+
height: 1000,
|
|
107
|
+
borderRadius: 0,
|
|
108
|
+
captionHeight: 0,
|
|
109
|
+
pageIndex: 1,
|
|
110
|
+
zIndex: 20
|
|
111
|
+
}]
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
validateArticleCoverControls,
|
|
116
|
+
createArticleCoverConfig,
|
|
117
|
+
DEFAULT_ARTICLE_COVER_CONTROLS,
|
|
118
|
+
ARTICLE_COVER_IMAGE_KEY,
|
|
119
|
+
ARTICLE_COVER_FONT
|
|
120
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{useId as w}from"react";import{jsx as r,jsxs as m}from"react/jsx-runtime";function z({value:p,onChange:k,disabled:q=!1}){let f=w();return m("fieldset",{className:"typeset-cover-controls",disabled:q,children:[r("legend",{children:"文章封面"}),r("label",{htmlFor:`${f}-background`,children:"背景色"}),r("input",{id:`${f}-background`,type:"color",value:p.backgroundColor,onChange:(y)=>k({...p,backgroundColor:y.target.value})}),r("label",{htmlFor:`${f}-title`,children:"标题色"}),r("input",{id:`${f}-title`,type:"color",value:p.titleColor,onChange:(y)=>k({...p,titleColor:y.target.value})}),r("label",{htmlFor:`${f}-size`,children:"标题字号"}),r("input",{id:`${f}-size`,type:"number",min:80,max:200,step:1,value:p.fontSize,onChange:(y)=>k({...p,fontSize:Number(y.target.value)})}),r("label",{htmlFor:`${f}-align`,children:"水平对齐"}),m("select",{id:`${f}-align`,value:p.textAlign,onChange:(y)=>k({...p,textAlign:y.target.value}),children:[r("option",{value:"left",children:"左对齐"}),r("option",{value:"center",children:"居中"}),r("option",{value:"right",children:"右对齐"})]}),r("label",{htmlFor:`${f}-padding`,children:"安全留白"}),r("input",{id:`${f}-padding`,type:"number",min:60,max:200,step:1,value:p.safePadding,onChange:(y)=>k({...p,safePadding:Number(y.target.value)})}),r("p",{children:"画布 3350×1000:左侧 2350 为标题,右侧 1000 为中心裁切方图。字体固定,预览以服务端 PNG 为准。"})]})}export{z as CoverControls};
|
package/dist/index.html
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
rel="stylesheet"
|
|
12
12
|
/>
|
|
13
13
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-web/style.css" />
|
|
14
|
-
<script type="module" crossorigin src="/assets/index-
|
|
14
|
+
<script type="module" crossorigin src="/assets/index-DMxjiZmE.js"></script>
|
|
15
15
|
<link rel="stylesheet" crossorigin href="/assets/index-JCv_QmUn.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body class="bg-[#fafafa] dark:bg-[#000000] text-[#171717] dark:text-[#ededed] antialiased selection:bg-[#eaeaea] selection:text-[#000000] dark:selection:bg-[#333333] dark:selection:text-[#ffffff] overflow-hidden">
|
package/dist/lib/adapter.js
CHANGED
|
@@ -5,11 +5,12 @@ import {
|
|
|
5
5
|
renderTypesetResultToPngs,
|
|
6
6
|
resolveCanvasFont,
|
|
7
7
|
resolveCanvasFontFamily
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-dz9pmgbk.js";
|
|
9
9
|
import {
|
|
10
10
|
registerServerFonts,
|
|
11
11
|
scanLocalFonts
|
|
12
|
-
} from "./index-
|
|
12
|
+
} from "./index-2vgqm8ms.js";
|
|
13
|
+
import"./index-xg6pyfw6.js";
|
|
13
14
|
import"./index-37x76zdn.js";
|
|
14
15
|
export {
|
|
15
16
|
scanLocalFonts,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARTICLE_COVER_FONT,
|
|
3
|
+
registerArticleCoverFont
|
|
4
|
+
} from "./index-r4swepwa.js";
|
|
1
5
|
import {
|
|
2
6
|
DEFAULT_BORDER,
|
|
3
7
|
DEFAULT_PATTERN,
|
|
@@ -7,14 +11,14 @@ import {
|
|
|
7
11
|
renderTypesetResultToPngs,
|
|
8
12
|
resolveDataDir,
|
|
9
13
|
validateCanvasStyle
|
|
10
|
-
} from "./index-
|
|
14
|
+
} from "./index-dz9pmgbk.js";
|
|
11
15
|
import {
|
|
12
16
|
buildLineTextFromRange,
|
|
13
17
|
getLineTextCache,
|
|
14
18
|
measureNaturalWidth,
|
|
15
19
|
prepareWithSegments,
|
|
16
20
|
stepPreparedLineGeometry
|
|
17
|
-
} from "./index-
|
|
21
|
+
} from "./index-xg6pyfw6.js";
|
|
18
22
|
import {
|
|
19
23
|
__require
|
|
20
24
|
} from "./index-37x76zdn.js";
|
|
@@ -3341,6 +3345,12 @@ function resolveTypesetResult(body) {
|
|
|
3341
3345
|
chrome: { ...baseConfig.chrome, ...overrides?.chrome }
|
|
3342
3346
|
};
|
|
3343
3347
|
const resolvedImages = applyTemplateImages(body.images || [], effectiveConfig.images, body.templateImageOverrides);
|
|
3348
|
+
if (effectiveConfig.typography.headingFont === ARTICLE_COVER_FONT.family || effectiveConfig.typography.bodyFont === ARTICLE_COVER_FONT.family) {
|
|
3349
|
+
const path = process.env.TYPESET_ARTICLE_COVER_FONT_PATH;
|
|
3350
|
+
if (!path)
|
|
3351
|
+
throw new Error("Article cover font path is not configured for this renderer");
|
|
3352
|
+
registerArticleCoverFont({ path });
|
|
3353
|
+
}
|
|
3344
3354
|
let result;
|
|
3345
3355
|
while (true) {
|
|
3346
3356
|
try {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearCache
|
|
3
|
+
} from "./index-xg6pyfw6.js";
|
|
4
|
+
|
|
5
|
+
// src/core/font-loader.ts
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import { basename, extname, join, resolve } from "node:path";
|
|
9
|
+
function readFontFamilyName(filePath) {
|
|
10
|
+
try {
|
|
11
|
+
const buf = readFileSync(filePath);
|
|
12
|
+
if (buf.length < 12)
|
|
13
|
+
return null;
|
|
14
|
+
const numTables = buf.readUInt16BE(4);
|
|
15
|
+
let nameTableOffset = 0;
|
|
16
|
+
for (let i = 0;i < numTables; i++) {
|
|
17
|
+
const offset = 12 + i * 16;
|
|
18
|
+
if (offset + 16 > buf.length)
|
|
19
|
+
break;
|
|
20
|
+
const tag = buf.toString("utf8", offset, offset + 4);
|
|
21
|
+
if (tag === "name") {
|
|
22
|
+
nameTableOffset = buf.readUInt32BE(offset + 8);
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (!nameTableOffset || nameTableOffset + 6 > buf.length)
|
|
27
|
+
return null;
|
|
28
|
+
const count = buf.readUInt16BE(nameTableOffset + 2);
|
|
29
|
+
const stringOffset = nameTableOffset + buf.readUInt16BE(nameTableOffset + 4);
|
|
30
|
+
let familyName = null;
|
|
31
|
+
let preferredFamily = null;
|
|
32
|
+
for (let i = 0;i < count; i++) {
|
|
33
|
+
const recOffset = nameTableOffset + 6 + i * 12;
|
|
34
|
+
if (recOffset + 12 > buf.length)
|
|
35
|
+
break;
|
|
36
|
+
const platformId = buf.readUInt16BE(recOffset);
|
|
37
|
+
const nameId = buf.readUInt16BE(recOffset + 6);
|
|
38
|
+
const length = buf.readUInt16BE(recOffset + 8);
|
|
39
|
+
const strOffset = stringOffset + buf.readUInt16BE(recOffset + 10);
|
|
40
|
+
if (strOffset + length > buf.length)
|
|
41
|
+
continue;
|
|
42
|
+
if (nameId === 1 || nameId === 16) {
|
|
43
|
+
let str = "";
|
|
44
|
+
if (platformId === 0 || platformId === 3) {
|
|
45
|
+
if (length % 2 !== 0)
|
|
46
|
+
continue;
|
|
47
|
+
str = Buffer.from(buf.subarray(strOffset, strOffset + length)).swap16().toString("utf16le").trim();
|
|
48
|
+
} else {
|
|
49
|
+
str = buf.toString("utf8", strOffset, strOffset + length).trim();
|
|
50
|
+
}
|
|
51
|
+
str = str.replace(/\0/g, "").trim();
|
|
52
|
+
if (str) {
|
|
53
|
+
if (nameId === 16)
|
|
54
|
+
preferredFamily = str;
|
|
55
|
+
if (nameId === 1 && !familyName)
|
|
56
|
+
familyName = str;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return preferredFamily || familyName || null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
var SUPPORTED_EXTS = new Set([".ttf", ".otf", ".woff2", ".woff"]);
|
|
66
|
+
var FORMAT_MAP = {
|
|
67
|
+
".ttf": "truetype",
|
|
68
|
+
".otf": "opentype",
|
|
69
|
+
".woff2": "woff2",
|
|
70
|
+
".woff": "woff"
|
|
71
|
+
};
|
|
72
|
+
var cachedFonts = null;
|
|
73
|
+
var registeredServerFonts = false;
|
|
74
|
+
function scanLocalFonts(options) {
|
|
75
|
+
if (cachedFonts && !options?.refresh) {
|
|
76
|
+
return cachedFonts;
|
|
77
|
+
}
|
|
78
|
+
const cwd = options?.cwd || process.cwd();
|
|
79
|
+
const searchDirs = [
|
|
80
|
+
join(cwd, "public", "fonts"),
|
|
81
|
+
join(cwd, "dist", "fonts"),
|
|
82
|
+
join(cwd, "fonts")
|
|
83
|
+
];
|
|
84
|
+
const foundMap = new Map;
|
|
85
|
+
const seenFilenames = new Set;
|
|
86
|
+
for (const dir of searchDirs) {
|
|
87
|
+
if (!existsSync(dir))
|
|
88
|
+
continue;
|
|
89
|
+
let manifest = {};
|
|
90
|
+
const manifestPath = join(dir, "fonts.json");
|
|
91
|
+
if (existsSync(manifestPath)) {
|
|
92
|
+
try {
|
|
93
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
94
|
+
} catch (err) {
|
|
95
|
+
console.warn(`[font-loader] Failed to parse manifest ${manifestPath}:`, err);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let files = [];
|
|
99
|
+
try {
|
|
100
|
+
files = readdirSync(dir);
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const filename of files) {
|
|
105
|
+
if (seenFilenames.has(filename))
|
|
106
|
+
continue;
|
|
107
|
+
const ext = extname(filename).toLowerCase();
|
|
108
|
+
if (!SUPPORTED_EXTS.has(ext))
|
|
109
|
+
continue;
|
|
110
|
+
const fullPath = resolve(dir, filename);
|
|
111
|
+
let fileStat;
|
|
112
|
+
try {
|
|
113
|
+
fileStat = statSync(fullPath);
|
|
114
|
+
if (!fileStat.isFile())
|
|
115
|
+
continue;
|
|
116
|
+
} catch {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
seenFilenames.add(filename);
|
|
120
|
+
const rawBase = basename(filename, ext);
|
|
121
|
+
const customMeta = manifest[filename] || manifest[rawBase] || {};
|
|
122
|
+
const parsedFamily = ext === ".ttf" || ext === ".otf" ? readFontFamilyName(fullPath) : null;
|
|
123
|
+
const family = customMeta.family || parsedFamily || rawBase;
|
|
124
|
+
const label = customMeta.label || `${family} (本地字体)`;
|
|
125
|
+
if (!foundMap.has(family)) {
|
|
126
|
+
foundMap.set(family, {
|
|
127
|
+
family,
|
|
128
|
+
label,
|
|
129
|
+
filename,
|
|
130
|
+
path: fullPath,
|
|
131
|
+
url: `/fonts/${encodeURIComponent(filename)}`,
|
|
132
|
+
format: FORMAT_MAP[ext] || "truetype",
|
|
133
|
+
fileSizeBytes: fileStat.size
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
cachedFonts = Array.from(foundMap.values());
|
|
139
|
+
return cachedFonts;
|
|
140
|
+
}
|
|
141
|
+
function registerServerFonts(options) {
|
|
142
|
+
const fonts = scanLocalFonts({ refresh: options?.refresh });
|
|
143
|
+
if (registeredServerFonts && !options?.refresh) {
|
|
144
|
+
return fonts;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
const { GlobalFonts } = createRequire(import.meta.url)("@napi-rs/canvas");
|
|
148
|
+
if (GlobalFonts && typeof GlobalFonts.registerFromPath === "function") {
|
|
149
|
+
for (const font of fonts) {
|
|
150
|
+
try {
|
|
151
|
+
GlobalFonts.registerFromPath(font.path, font.family);
|
|
152
|
+
const rawBase = basename(font.filename, extname(font.filename));
|
|
153
|
+
if (rawBase !== font.family) {
|
|
154
|
+
GlobalFonts.registerFromPath(font.path, rawBase);
|
|
155
|
+
}
|
|
156
|
+
} catch (err) {
|
|
157
|
+
console.warn(`[font-loader] Failed to register font ${font.filename} in Skia:`, err);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
registeredServerFonts = true;
|
|
161
|
+
clearCache();
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
return fonts;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { readFontFamilyName, scanLocalFonts, registerServerFonts };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
registerServerFonts
|
|
3
|
-
} from "./index-
|
|
3
|
+
} from "./index-2vgqm8ms.js";
|
|
4
4
|
|
|
5
5
|
// src/core/canvas-renderer.ts
|
|
6
6
|
import { createCanvas, loadImage } from "@napi-rs/canvas";
|
|
@@ -509,7 +509,17 @@ async function drawPageToContext(ctx, options) {
|
|
|
509
509
|
ctx.roundRect(-w / 2, -h / 2, w, h, radius);
|
|
510
510
|
ctx.clip();
|
|
511
511
|
if (loadedImg) {
|
|
512
|
-
|
|
512
|
+
if (img.fit === "cover") {
|
|
513
|
+
const sourceWidth = loadedImg.naturalWidth || loadedImg.width;
|
|
514
|
+
const sourceHeight = loadedImg.naturalHeight || loadedImg.height;
|
|
515
|
+
if (!(sourceWidth > 0 && sourceHeight > 0))
|
|
516
|
+
throw new Error("Invalid cover image dimensions");
|
|
517
|
+
const scale = Math.max(w / sourceWidth, h / sourceHeight);
|
|
518
|
+
const cropWidth = w / scale;
|
|
519
|
+
const cropHeight = h / scale;
|
|
520
|
+
ctx.drawImage(loadedImg, (sourceWidth - cropWidth) / 2, (sourceHeight - cropHeight) / 2, cropWidth, cropHeight, -w / 2, -h / 2, w, h);
|
|
521
|
+
} else
|
|
522
|
+
ctx.drawImage(loadedImg, -w / 2, -h / 2, w, h);
|
|
513
523
|
} else {
|
|
514
524
|
ctx.fillStyle = theme.cardBg || "#222222";
|
|
515
525
|
ctx.fillRect(-w / 2, -h / 2, w, h);
|
|
@@ -103,8 +103,8 @@ async function renderRemoteBatch(studioUrl, body, outputDir, route, kind) {
|
|
|
103
103
|
}
|
|
104
104
|
async function renderLocalBatch(body, outputDir, route, kind) {
|
|
105
105
|
const [{ resolveTypesetResult }, { renderTypesetResultToPngs }] = await Promise.all([
|
|
106
|
-
import("./render-
|
|
107
|
-
import("./canvas-renderer-
|
|
106
|
+
import("./render-fvtcq3hb.js"),
|
|
107
|
+
import("./canvas-renderer-hpe2bqkt.js")
|
|
108
108
|
]);
|
|
109
109
|
const { result } = resolveTypesetResult(body);
|
|
110
110
|
const pngs = await renderTypesetResultToPngs(result);
|
|
@@ -131,7 +131,7 @@ async function renderLocalBatch(body, outputDir, route, kind) {
|
|
|
131
131
|
}
|
|
132
132
|
var typesetStudioAdapter = {
|
|
133
133
|
name: "typeset-studio",
|
|
134
|
-
version: "0.
|
|
134
|
+
version: "0.3.0",
|
|
135
135
|
async doctor() {
|
|
136
136
|
const checks = [];
|
|
137
137
|
const REMOTE_STUDIO_URL = remoteStudioUrl();
|
|
@@ -175,7 +175,7 @@ var typesetStudioAdapter = {
|
|
|
175
175
|
});
|
|
176
176
|
}
|
|
177
177
|
try {
|
|
178
|
-
const { scanLocalFonts } = await import("./font-loader-
|
|
178
|
+
const { scanLocalFonts } = await import("./font-loader-cft81crk.js");
|
|
179
179
|
const localFonts = scanLocalFonts();
|
|
180
180
|
checks.push({
|
|
181
181
|
name: "local-fonts",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearCache
|
|
3
|
+
} from "./index-xg6pyfw6.js";
|
|
4
|
+
|
|
5
|
+
// src/server-font.ts
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { isAbsolute } from "node:path";
|
|
9
|
+
import { GlobalFonts } from "@napi-rs/canvas";
|
|
10
|
+
|
|
11
|
+
// src/core/article-cover.ts
|
|
12
|
+
var ARTICLE_COVER_FONT = Object.freeze({
|
|
13
|
+
id: "publishing-noto-cjk-regular-v1",
|
|
14
|
+
sha256: "b76b0433203017ca80401b2ee0dd69350349871c4b19d504c34dbdd80541690a",
|
|
15
|
+
family: "Publishing Cover"
|
|
16
|
+
});
|
|
17
|
+
var DEFAULT_ARTICLE_COVER_CONTROLS = Object.freeze({
|
|
18
|
+
backgroundColor: "#ffffff",
|
|
19
|
+
titleColor: "#171717",
|
|
20
|
+
fontSize: 120,
|
|
21
|
+
textAlign: "left",
|
|
22
|
+
safePadding: 120
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// src/server-font.ts
|
|
26
|
+
var registered = false;
|
|
27
|
+
function registerArticleCoverFont({ path }) {
|
|
28
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
29
|
+
throw new Error("Article cover font requires a trusted absolute path");
|
|
30
|
+
let bytes;
|
|
31
|
+
try {
|
|
32
|
+
bytes = readFileSync(path);
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error("Article cover font file is unavailable");
|
|
35
|
+
}
|
|
36
|
+
if (createHash("sha256").update(bytes).digest("hex") !== ARTICLE_COVER_FONT.sha256)
|
|
37
|
+
throw new Error("Article cover font hash mismatch");
|
|
38
|
+
if (!registered || !GlobalFonts.has(ARTICLE_COVER_FONT.family)) {
|
|
39
|
+
if (!GlobalFonts.registerFromPath(path, ARTICLE_COVER_FONT.family) || !GlobalFonts.has(ARTICLE_COVER_FONT.family))
|
|
40
|
+
throw new Error("Article cover font registration failed");
|
|
41
|
+
registered = true;
|
|
42
|
+
clearCache();
|
|
43
|
+
}
|
|
44
|
+
return ARTICLE_COVER_FONT;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { ARTICLE_COVER_FONT, registerArticleCoverFont };
|
|
@@ -3715,166 +3715,4 @@ function clearCache() {
|
|
|
3715
3715
|
clearMeasurementCaches();
|
|
3716
3716
|
}
|
|
3717
3717
|
|
|
3718
|
-
|
|
3719
|
-
import { createRequire } from "node:module";
|
|
3720
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3721
|
-
import { basename, extname, join, resolve } from "node:path";
|
|
3722
|
-
function readFontFamilyName(filePath) {
|
|
3723
|
-
try {
|
|
3724
|
-
const buf = readFileSync(filePath);
|
|
3725
|
-
if (buf.length < 12)
|
|
3726
|
-
return null;
|
|
3727
|
-
const numTables = buf.readUInt16BE(4);
|
|
3728
|
-
let nameTableOffset = 0;
|
|
3729
|
-
for (let i = 0;i < numTables; i++) {
|
|
3730
|
-
const offset = 12 + i * 16;
|
|
3731
|
-
if (offset + 16 > buf.length)
|
|
3732
|
-
break;
|
|
3733
|
-
const tag = buf.toString("utf8", offset, offset + 4);
|
|
3734
|
-
if (tag === "name") {
|
|
3735
|
-
nameTableOffset = buf.readUInt32BE(offset + 8);
|
|
3736
|
-
break;
|
|
3737
|
-
}
|
|
3738
|
-
}
|
|
3739
|
-
if (!nameTableOffset || nameTableOffset + 6 > buf.length)
|
|
3740
|
-
return null;
|
|
3741
|
-
const count = buf.readUInt16BE(nameTableOffset + 2);
|
|
3742
|
-
const stringOffset = nameTableOffset + buf.readUInt16BE(nameTableOffset + 4);
|
|
3743
|
-
let familyName = null;
|
|
3744
|
-
let preferredFamily = null;
|
|
3745
|
-
for (let i = 0;i < count; i++) {
|
|
3746
|
-
const recOffset = nameTableOffset + 6 + i * 12;
|
|
3747
|
-
if (recOffset + 12 > buf.length)
|
|
3748
|
-
break;
|
|
3749
|
-
const platformId = buf.readUInt16BE(recOffset);
|
|
3750
|
-
const nameId = buf.readUInt16BE(recOffset + 6);
|
|
3751
|
-
const length = buf.readUInt16BE(recOffset + 8);
|
|
3752
|
-
const strOffset = stringOffset + buf.readUInt16BE(recOffset + 10);
|
|
3753
|
-
if (strOffset + length > buf.length)
|
|
3754
|
-
continue;
|
|
3755
|
-
if (nameId === 1 || nameId === 16) {
|
|
3756
|
-
let str = "";
|
|
3757
|
-
if (platformId === 0 || platformId === 3) {
|
|
3758
|
-
if (length % 2 !== 0)
|
|
3759
|
-
continue;
|
|
3760
|
-
str = Buffer.from(buf.subarray(strOffset, strOffset + length)).swap16().toString("utf16le").trim();
|
|
3761
|
-
} else {
|
|
3762
|
-
str = buf.toString("utf8", strOffset, strOffset + length).trim();
|
|
3763
|
-
}
|
|
3764
|
-
str = str.replace(/\0/g, "").trim();
|
|
3765
|
-
if (str) {
|
|
3766
|
-
if (nameId === 16)
|
|
3767
|
-
preferredFamily = str;
|
|
3768
|
-
if (nameId === 1 && !familyName)
|
|
3769
|
-
familyName = str;
|
|
3770
|
-
}
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
return preferredFamily || familyName || null;
|
|
3774
|
-
} catch {
|
|
3775
|
-
return null;
|
|
3776
|
-
}
|
|
3777
|
-
}
|
|
3778
|
-
var SUPPORTED_EXTS = new Set([".ttf", ".otf", ".woff2", ".woff"]);
|
|
3779
|
-
var FORMAT_MAP = {
|
|
3780
|
-
".ttf": "truetype",
|
|
3781
|
-
".otf": "opentype",
|
|
3782
|
-
".woff2": "woff2",
|
|
3783
|
-
".woff": "woff"
|
|
3784
|
-
};
|
|
3785
|
-
var cachedFonts = null;
|
|
3786
|
-
var registeredServerFonts = false;
|
|
3787
|
-
function scanLocalFonts(options) {
|
|
3788
|
-
if (cachedFonts && !options?.refresh) {
|
|
3789
|
-
return cachedFonts;
|
|
3790
|
-
}
|
|
3791
|
-
const cwd = options?.cwd || process.cwd();
|
|
3792
|
-
const searchDirs = [
|
|
3793
|
-
join(cwd, "public", "fonts"),
|
|
3794
|
-
join(cwd, "dist", "fonts"),
|
|
3795
|
-
join(cwd, "fonts")
|
|
3796
|
-
];
|
|
3797
|
-
const foundMap = new Map;
|
|
3798
|
-
const seenFilenames = new Set;
|
|
3799
|
-
for (const dir of searchDirs) {
|
|
3800
|
-
if (!existsSync(dir))
|
|
3801
|
-
continue;
|
|
3802
|
-
let manifest = {};
|
|
3803
|
-
const manifestPath = join(dir, "fonts.json");
|
|
3804
|
-
if (existsSync(manifestPath)) {
|
|
3805
|
-
try {
|
|
3806
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
3807
|
-
} catch (err) {
|
|
3808
|
-
console.warn(`[font-loader] Failed to parse manifest ${manifestPath}:`, err);
|
|
3809
|
-
}
|
|
3810
|
-
}
|
|
3811
|
-
let files = [];
|
|
3812
|
-
try {
|
|
3813
|
-
files = readdirSync(dir);
|
|
3814
|
-
} catch {
|
|
3815
|
-
continue;
|
|
3816
|
-
}
|
|
3817
|
-
for (const filename of files) {
|
|
3818
|
-
if (seenFilenames.has(filename))
|
|
3819
|
-
continue;
|
|
3820
|
-
const ext = extname(filename).toLowerCase();
|
|
3821
|
-
if (!SUPPORTED_EXTS.has(ext))
|
|
3822
|
-
continue;
|
|
3823
|
-
const fullPath = resolve(dir, filename);
|
|
3824
|
-
let fileStat;
|
|
3825
|
-
try {
|
|
3826
|
-
fileStat = statSync(fullPath);
|
|
3827
|
-
if (!fileStat.isFile())
|
|
3828
|
-
continue;
|
|
3829
|
-
} catch {
|
|
3830
|
-
continue;
|
|
3831
|
-
}
|
|
3832
|
-
seenFilenames.add(filename);
|
|
3833
|
-
const rawBase = basename(filename, ext);
|
|
3834
|
-
const customMeta = manifest[filename] || manifest[rawBase] || {};
|
|
3835
|
-
const parsedFamily = ext === ".ttf" || ext === ".otf" ? readFontFamilyName(fullPath) : null;
|
|
3836
|
-
const family = customMeta.family || parsedFamily || rawBase;
|
|
3837
|
-
const label = customMeta.label || `${family} (本地字体)`;
|
|
3838
|
-
if (!foundMap.has(family)) {
|
|
3839
|
-
foundMap.set(family, {
|
|
3840
|
-
family,
|
|
3841
|
-
label,
|
|
3842
|
-
filename,
|
|
3843
|
-
path: fullPath,
|
|
3844
|
-
url: `/fonts/${encodeURIComponent(filename)}`,
|
|
3845
|
-
format: FORMAT_MAP[ext] || "truetype",
|
|
3846
|
-
fileSizeBytes: fileStat.size
|
|
3847
|
-
});
|
|
3848
|
-
}
|
|
3849
|
-
}
|
|
3850
|
-
}
|
|
3851
|
-
cachedFonts = Array.from(foundMap.values());
|
|
3852
|
-
return cachedFonts;
|
|
3853
|
-
}
|
|
3854
|
-
function registerServerFonts(options) {
|
|
3855
|
-
const fonts = scanLocalFonts({ refresh: options?.refresh });
|
|
3856
|
-
if (registeredServerFonts && !options?.refresh) {
|
|
3857
|
-
return fonts;
|
|
3858
|
-
}
|
|
3859
|
-
try {
|
|
3860
|
-
const { GlobalFonts } = createRequire(import.meta.url)("@napi-rs/canvas");
|
|
3861
|
-
if (GlobalFonts && typeof GlobalFonts.registerFromPath === "function") {
|
|
3862
|
-
for (const font of fonts) {
|
|
3863
|
-
try {
|
|
3864
|
-
GlobalFonts.registerFromPath(font.path, font.family);
|
|
3865
|
-
const rawBase = basename(font.filename, extname(font.filename));
|
|
3866
|
-
if (rawBase !== font.family) {
|
|
3867
|
-
GlobalFonts.registerFromPath(font.path, rawBase);
|
|
3868
|
-
}
|
|
3869
|
-
} catch (err) {
|
|
3870
|
-
console.warn(`[font-loader] Failed to register font ${font.filename} in Skia:`, err);
|
|
3871
|
-
}
|
|
3872
|
-
}
|
|
3873
|
-
registeredServerFonts = true;
|
|
3874
|
-
clearCache();
|
|
3875
|
-
}
|
|
3876
|
-
} catch {}
|
|
3877
|
-
return fonts;
|
|
3878
|
-
}
|
|
3879
|
-
|
|
3880
|
-
export { stepPreparedLineGeometry, getLineTextCache, buildLineTextFromRange, prepareWithSegments, measureNaturalWidth, readFontFamilyName, scanLocalFonts, registerServerFonts };
|
|
3718
|
+
export { stepPreparedLineGeometry, getLineTextCache, buildLineTextFromRange, prepareWithSegments, measureNaturalWidth, clearCache };
|
package/dist/lib/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
plugin,
|
|
3
3
|
typesetStudioAdapter
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-q952vprv.js";
|
|
5
5
|
import {
|
|
6
6
|
applyTemplateImages,
|
|
7
7
|
getDefaultConfig,
|
|
8
8
|
resolveTypesetResult,
|
|
9
9
|
runTypeset,
|
|
10
10
|
validateTemplateAssets
|
|
11
|
-
} from "./index-
|
|
11
|
+
} from "./index-1yt7sbqe.js";
|
|
12
|
+
import"./index-r4swepwa.js";
|
|
12
13
|
import {
|
|
13
14
|
drawPageToContext,
|
|
14
15
|
renderPageToCanvas,
|
|
@@ -16,12 +17,13 @@ import {
|
|
|
16
17
|
renderTypesetResultToPngs,
|
|
17
18
|
resolveCanvasFont,
|
|
18
19
|
resolveCanvasFontFamily
|
|
19
|
-
} from "./index-
|
|
20
|
+
} from "./index-dz9pmgbk.js";
|
|
20
21
|
import {
|
|
21
22
|
readFontFamilyName,
|
|
22
23
|
registerServerFonts,
|
|
23
24
|
scanLocalFonts
|
|
24
|
-
} from "./index-
|
|
25
|
+
} from "./index-2vgqm8ms.js";
|
|
26
|
+
import"./index-xg6pyfw6.js";
|
|
25
27
|
import"./index-37x76zdn.js";
|
|
26
28
|
export {
|
|
27
29
|
validateTemplateAssets,
|