@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.
@@ -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-Dn5YtK3W.js"></script>
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">
@@ -2,7 +2,7 @@ import {
2
2
  adapter_default,
3
3
  plugin,
4
4
  typesetStudioAdapter
5
- } from "./index-y2tk7642.js";
5
+ } from "./index-q952vprv.js";
6
6
  import"./index-37x76zdn.js";
7
7
  export {
8
8
  typesetStudioAdapter,
@@ -5,11 +5,12 @@ import {
5
5
  renderTypesetResultToPngs,
6
6
  resolveCanvasFont,
7
7
  resolveCanvasFontFamily
8
- } from "./index-697fkv5y.js";
8
+ } from "./index-dz9pmgbk.js";
9
9
  import {
10
10
  registerServerFonts,
11
11
  scanLocalFonts
12
- } from "./index-6c43c7h4.js";
12
+ } from "./index-2vgqm8ms.js";
13
+ import"./index-xg6pyfw6.js";
13
14
  import"./index-37x76zdn.js";
14
15
  export {
15
16
  scanLocalFonts,
@@ -2,7 +2,8 @@ import {
2
2
  readFontFamilyName,
3
3
  registerServerFonts,
4
4
  scanLocalFonts
5
- } from "./index-6c43c7h4.js";
5
+ } from "./index-2vgqm8ms.js";
6
+ import"./index-xg6pyfw6.js";
6
7
  import"./index-37x76zdn.js";
7
8
  export {
8
9
  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-697fkv5y.js";
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-6c43c7h4.js";
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-6c43c7h4.js";
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
- ctx.drawImage(loadedImg, -w / 2, -h / 2, w, h);
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-064weavx.js"),
107
- import("./canvas-renderer-wce6xgb2.js")
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.2.8",
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-j28mt43t.js");
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
- // src/core/font-loader.ts
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-y2tk7642.js";
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-qszn98j2.js";
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-697fkv5y.js";
20
+ } from "./index-dz9pmgbk.js";
20
21
  import {
21
22
  readFontFamilyName,
22
23
  registerServerFonts,
23
24
  scanLocalFonts
24
- } from "./index-6c43c7h4.js";
25
+ } from "./index-2vgqm8ms.js";
26
+ import"./index-xg6pyfw6.js";
25
27
  import"./index-37x76zdn.js";
26
28
  export {
27
29
  validateTemplateAssets,