@akairos/typeset-studio 0.2.8 → 0.3.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 +20 -0
- package/dist/assets/{index-Dn5YtK3W.js → index-CrJAVx3W.js} +17 -17
- package/dist/cover/cover.js +124 -0
- package/dist/editor/editor.js +1 -0
- package/dist/index.html +1 -1
- package/dist/lib/adapter.js +2 -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-2vgqm8ms.js +167 -0
- package/dist/lib/{index-qszn98j2.js → index-3ptjkctd.js} +14 -2
- package/dist/lib/{index-y2tk7642.js → index-51d5rpf7.js} +11 -5
- package/dist/lib/index-94yttj49.js +34 -0
- package/dist/lib/{index-697fkv5y.js → index-dz9pmgbk.js} +12 -2
- package/dist/lib/index-nwch18wp.js +18 -0
- package/dist/lib/{index-6c43c7h4.js → index-xg6pyfw6.js} +1 -163
- package/dist/lib/index.js +7 -4
- package/dist/lib/render-epbcmzxe.js +14 -0
- package/dist/lib/server-font.js +9 -0
- package/package.json +16 -2
- package/src/adapter.test.ts +41 -0
- package/src/adapter.ts +5 -2
- package/src/core/article-cover.test.ts +47 -0
- package/src/core/article-cover.ts +97 -0
- package/src/core/canvas-draw.ts +9 -1
- package/src/core/types.ts +1 -0
- package/src/cover.ts +10 -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,124 @@
|
|
|
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 normalizeArticleCoverTitle(raw) {
|
|
16
|
+
return raw.trim().replace(/^(?:#+\s*)+/, "").trim();
|
|
17
|
+
}
|
|
18
|
+
function object(value) {
|
|
19
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
20
|
+
throw new RangeError("Invalid article cover object");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
function exactKeys(value, keys) {
|
|
24
|
+
if (Object.keys(value).length !== keys.length || Object.keys(value).some((key) => !keys.includes(key)))
|
|
25
|
+
throw new RangeError("Unsupported article cover fields");
|
|
26
|
+
}
|
|
27
|
+
function color(value) {
|
|
28
|
+
if (typeof value !== "string" || !/^#[a-f\d]{6}$/i.test(value))
|
|
29
|
+
throw new RangeError("Article cover colors must be six-digit hex");
|
|
30
|
+
return value.toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
function validateArticleCoverControls(value) {
|
|
33
|
+
const input = object(value);
|
|
34
|
+
exactKeys(input, ["backgroundColor", "titleColor", "fontSize", "textAlign", "safePadding"]);
|
|
35
|
+
if (!Number.isInteger(input.fontSize) || input.fontSize < 80 || input.fontSize > 200)
|
|
36
|
+
throw new RangeError("Article cover fontSize must be an integer between 80 and 200");
|
|
37
|
+
if (!Number.isInteger(input.safePadding) || input.safePadding < 60 || input.safePadding > 200)
|
|
38
|
+
throw new RangeError("Article cover safePadding must be an integer between 60 and 200");
|
|
39
|
+
if (!["left", "center", "right"].includes(input.textAlign))
|
|
40
|
+
throw new RangeError("Invalid article cover textAlign");
|
|
41
|
+
return {
|
|
42
|
+
backgroundColor: color(input.backgroundColor),
|
|
43
|
+
titleColor: color(input.titleColor),
|
|
44
|
+
fontSize: input.fontSize,
|
|
45
|
+
textAlign: input.textAlign,
|
|
46
|
+
safePadding: input.safePadding
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function createArticleCoverConfig(value) {
|
|
50
|
+
const input = object(value);
|
|
51
|
+
exactKeys(input, ["id", "name", "controls", "assetId"]);
|
|
52
|
+
const uuid = /^[a-f\d]{8}(-[a-f\d]{4}){3}-[a-f\d]{12}$/i;
|
|
53
|
+
if (typeof input.id !== "string" || !uuid.test(input.id) || typeof input.assetId !== "string" || !uuid.test(input.assetId))
|
|
54
|
+
throw new RangeError("Article cover and asset ids must be UUIDs");
|
|
55
|
+
if (typeof input.name !== "string" || !input.name.trim() || input.name.trim().length > 80)
|
|
56
|
+
throw new RangeError("Article cover name must contain 1 to 80 characters");
|
|
57
|
+
const controls = validateArticleCoverControls(input.controls);
|
|
58
|
+
const safeArea = { top: controls.safePadding, right: 1000 + controls.safePadding, bottom: controls.safePadding, left: controls.safePadding };
|
|
59
|
+
const font = { fontFamily: ARTICLE_COVER_FONT.family, fontSize: controls.fontSize, minFontSize: controls.fontSize, fontWeight: 400, lineHeight: 1.2, letterSpacing: 0 };
|
|
60
|
+
return {
|
|
61
|
+
id: input.id,
|
|
62
|
+
name: input.name.trim(),
|
|
63
|
+
slot: "article_cover",
|
|
64
|
+
aspectRatio: "wechat-cover-2.35",
|
|
65
|
+
algorithm: {
|
|
66
|
+
baseFontSize: controls.fontSize,
|
|
67
|
+
scaleRatio: 1,
|
|
68
|
+
lineHeightMultiplier: 1.2,
|
|
69
|
+
obstacleGap: 0,
|
|
70
|
+
targetFillRatio: 1,
|
|
71
|
+
contentBottomGap: 0,
|
|
72
|
+
textAlign: controls.textAlign,
|
|
73
|
+
verticalAlign: "center",
|
|
74
|
+
safeArea
|
|
75
|
+
},
|
|
76
|
+
theme: {
|
|
77
|
+
bgColor: controls.backgroundColor,
|
|
78
|
+
textColor: controls.titleColor,
|
|
79
|
+
headingColor: controls.titleColor,
|
|
80
|
+
accentColor: controls.titleColor,
|
|
81
|
+
quoteColor: controls.titleColor,
|
|
82
|
+
captionColor: controls.titleColor,
|
|
83
|
+
watermarkColor: controls.titleColor,
|
|
84
|
+
cardBg: controls.backgroundColor
|
|
85
|
+
},
|
|
86
|
+
typography: { headingFont: ARTICLE_COVER_FONT.family, bodyFont: ARTICLE_COVER_FONT.family },
|
|
87
|
+
footerText: "",
|
|
88
|
+
watermarkText: "",
|
|
89
|
+
chrome: { showHeader: false, showWatermark: false, showPageNumber: false, showFooter: false, showFooterDivider: false, showFooterText: false, showFillRatio: false },
|
|
90
|
+
coverTheme: {
|
|
91
|
+
id: input.id,
|
|
92
|
+
format: "wechat-cover-split",
|
|
93
|
+
typography: { title: { ...font }, subtitle: { ...font }, footer: { ...font } },
|
|
94
|
+
colors: { text: controls.titleColor, accent: controls.titleColor, muted: controls.titleColor },
|
|
95
|
+
background: { fill: { type: "solid", color: controls.backgroundColor }, image: null, overlay: null },
|
|
96
|
+
decoration: { pattern: "none", color: controls.titleColor, opacity: 0, size: 1, image: null },
|
|
97
|
+
layout: { textAlign: controls.textAlign, verticalAlign: "center", safeArea: { ...safeArea } }
|
|
98
|
+
},
|
|
99
|
+
images: [{
|
|
100
|
+
id: ARTICLE_COVER_IMAGE_KEY,
|
|
101
|
+
templateAssetKey: ARTICLE_COVER_IMAGE_KEY,
|
|
102
|
+
src: `asset:${input.assetId}`,
|
|
103
|
+
alt: "文章封面方图",
|
|
104
|
+
fit: "cover",
|
|
105
|
+
isAbsolute: true,
|
|
106
|
+
x: 2350 - controls.safePadding,
|
|
107
|
+
y: -controls.safePadding,
|
|
108
|
+
width: 1000,
|
|
109
|
+
height: 1000,
|
|
110
|
+
borderRadius: 0,
|
|
111
|
+
captionHeight: 0,
|
|
112
|
+
pageIndex: 1,
|
|
113
|
+
zIndex: 20
|
|
114
|
+
}]
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
export {
|
|
118
|
+
validateArticleCoverControls,
|
|
119
|
+
normalizeArticleCoverTitle,
|
|
120
|
+
createArticleCoverConfig,
|
|
121
|
+
DEFAULT_ARTICLE_COVER_CONTROLS,
|
|
122
|
+
ARTICLE_COVER_IMAGE_KEY,
|
|
123
|
+
ARTICLE_COVER_FONT
|
|
124
|
+
};
|
|
@@ -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-CrJAVx3W.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,
|
|
@@ -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,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerArticleCoverFont
|
|
3
|
+
} from "./index-94yttj49.js";
|
|
4
|
+
import {
|
|
5
|
+
ARTICLE_COVER_FONT
|
|
6
|
+
} from "./index-nwch18wp.js";
|
|
1
7
|
import {
|
|
2
8
|
DEFAULT_BORDER,
|
|
3
9
|
DEFAULT_PATTERN,
|
|
@@ -7,14 +13,14 @@ import {
|
|
|
7
13
|
renderTypesetResultToPngs,
|
|
8
14
|
resolveDataDir,
|
|
9
15
|
validateCanvasStyle
|
|
10
|
-
} from "./index-
|
|
16
|
+
} from "./index-dz9pmgbk.js";
|
|
11
17
|
import {
|
|
12
18
|
buildLineTextFromRange,
|
|
13
19
|
getLineTextCache,
|
|
14
20
|
measureNaturalWidth,
|
|
15
21
|
prepareWithSegments,
|
|
16
22
|
stepPreparedLineGeometry
|
|
17
|
-
} from "./index-
|
|
23
|
+
} from "./index-xg6pyfw6.js";
|
|
18
24
|
import {
|
|
19
25
|
__require
|
|
20
26
|
} from "./index-37x76zdn.js";
|
|
@@ -3341,6 +3347,12 @@ function resolveTypesetResult(body) {
|
|
|
3341
3347
|
chrome: { ...baseConfig.chrome, ...overrides?.chrome }
|
|
3342
3348
|
};
|
|
3343
3349
|
const resolvedImages = applyTemplateImages(body.images || [], effectiveConfig.images, body.templateImageOverrides);
|
|
3350
|
+
if (effectiveConfig.typography.headingFont === ARTICLE_COVER_FONT.family || effectiveConfig.typography.bodyFont === ARTICLE_COVER_FONT.family) {
|
|
3351
|
+
const path = process.env.TYPESET_ARTICLE_COVER_FONT_PATH;
|
|
3352
|
+
if (!path)
|
|
3353
|
+
throw new Error("Article cover font path is not configured for this renderer");
|
|
3354
|
+
registerArticleCoverFont({ path });
|
|
3355
|
+
}
|
|
3344
3356
|
let result;
|
|
3345
3357
|
while (true) {
|
|
3346
3358
|
try {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeArticleCoverTitle
|
|
3
|
+
} from "./index-nwch18wp.js";
|
|
1
4
|
import {
|
|
2
5
|
__require
|
|
3
6
|
} from "./index-37x76zdn.js";
|
|
@@ -103,8 +106,8 @@ async function renderRemoteBatch(studioUrl, body, outputDir, route, kind) {
|
|
|
103
106
|
}
|
|
104
107
|
async function renderLocalBatch(body, outputDir, route, kind) {
|
|
105
108
|
const [{ resolveTypesetResult }, { renderTypesetResultToPngs }] = await Promise.all([
|
|
106
|
-
import("./render-
|
|
107
|
-
import("./canvas-renderer-
|
|
109
|
+
import("./render-epbcmzxe.js"),
|
|
110
|
+
import("./canvas-renderer-hpe2bqkt.js")
|
|
108
111
|
]);
|
|
109
112
|
const { result } = resolveTypesetResult(body);
|
|
110
113
|
const pngs = await renderTypesetResultToPngs(result);
|
|
@@ -131,7 +134,7 @@ async function renderLocalBatch(body, outputDir, route, kind) {
|
|
|
131
134
|
}
|
|
132
135
|
var typesetStudioAdapter = {
|
|
133
136
|
name: "typeset-studio",
|
|
134
|
-
version: "0.
|
|
137
|
+
version: "0.3.1",
|
|
135
138
|
async doctor() {
|
|
136
139
|
const checks = [];
|
|
137
140
|
const REMOTE_STUDIO_URL = remoteStudioUrl();
|
|
@@ -175,7 +178,7 @@ var typesetStudioAdapter = {
|
|
|
175
178
|
});
|
|
176
179
|
}
|
|
177
180
|
try {
|
|
178
|
-
const { scanLocalFonts } = await import("./font-loader-
|
|
181
|
+
const { scanLocalFonts } = await import("./font-loader-cft81crk.js");
|
|
179
182
|
const localFonts = scanLocalFonts();
|
|
180
183
|
checks.push({
|
|
181
184
|
name: "local-fonts",
|
|
@@ -213,7 +216,10 @@ var typesetStudioAdapter = {
|
|
|
213
216
|
const fallbackIcon = visual.fallbackIcon ?? stateVisual?.fallback_icon;
|
|
214
217
|
const template = ["wechat-cover-split", "poster-3-4", "longform-3-4"].includes(input.template || "") ? undefined : input.template;
|
|
215
218
|
input.onProgress?.({ stage: "typeset", message: "开始排版", route, account });
|
|
216
|
-
const
|
|
219
|
+
const rawTitle = input.title || state.metadata?.title || "";
|
|
220
|
+
const title = process.env.ZZHUB_PIPELINE_COVER_SNAPSHOT === "1" ? normalizeArticleCoverTitle(rawTitle) : cleanTitle(rawTitle);
|
|
221
|
+
if (process.env.ZZHUB_PIPELINE_COVER_SNAPSHOT === "1" && !title)
|
|
222
|
+
throw new RangeError("Article cover title is empty after normalization");
|
|
217
223
|
const common = {
|
|
218
224
|
account,
|
|
219
225
|
title,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ARTICLE_COVER_FONT
|
|
3
|
+
} from "./index-nwch18wp.js";
|
|
4
|
+
import {
|
|
5
|
+
clearCache
|
|
6
|
+
} from "./index-xg6pyfw6.js";
|
|
7
|
+
|
|
8
|
+
// src/server-font.ts
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { isAbsolute } from "node:path";
|
|
12
|
+
import { GlobalFonts } from "@napi-rs/canvas";
|
|
13
|
+
var registered = false;
|
|
14
|
+
function registerArticleCoverFont({ path }) {
|
|
15
|
+
if (typeof path !== "string" || !isAbsolute(path))
|
|
16
|
+
throw new Error("Article cover font requires a trusted absolute path");
|
|
17
|
+
let bytes;
|
|
18
|
+
try {
|
|
19
|
+
bytes = readFileSync(path);
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error("Article cover font file is unavailable");
|
|
22
|
+
}
|
|
23
|
+
if (createHash("sha256").update(bytes).digest("hex") !== ARTICLE_COVER_FONT.sha256)
|
|
24
|
+
throw new Error("Article cover font hash mismatch");
|
|
25
|
+
if (!registered || !GlobalFonts.has(ARTICLE_COVER_FONT.family)) {
|
|
26
|
+
if (!GlobalFonts.registerFromPath(path, ARTICLE_COVER_FONT.family) || !GlobalFonts.has(ARTICLE_COVER_FONT.family))
|
|
27
|
+
throw new Error("Article cover font registration failed");
|
|
28
|
+
registered = true;
|
|
29
|
+
clearCache();
|
|
30
|
+
}
|
|
31
|
+
return ARTICLE_COVER_FONT;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { registerArticleCoverFont };
|
|
@@ -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);
|
|
@@ -0,0 +1,18 @@
|
|
|
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 DEFAULT_ARTICLE_COVER_CONTROLS = Object.freeze({
|
|
8
|
+
backgroundColor: "#ffffff",
|
|
9
|
+
titleColor: "#171717",
|
|
10
|
+
fontSize: 120,
|
|
11
|
+
textAlign: "left",
|
|
12
|
+
safePadding: 120
|
|
13
|
+
});
|
|
14
|
+
function normalizeArticleCoverTitle(raw) {
|
|
15
|
+
return raw.trim().replace(/^(?:#+\s*)+/, "").trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { ARTICLE_COVER_FONT, normalizeArticleCoverTitle };
|
|
@@ -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 };
|