@lyhue1991/wxgzh 0.1.4 → 0.2.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 CHANGED
@@ -415,6 +415,7 @@ enableComment: true
415
415
 
416
416
  ```bash
417
417
  wxgzh --help
418
+ wxgzh --version
418
419
  wxgzh article.md
419
420
  wxgzh config --list
420
421
  wxgzh config --list-themes
@@ -424,4 +425,3 @@ wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg
424
425
  wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg
425
426
  ```
426
427
 
427
-
package/dist/cli/index.js CHANGED
@@ -36,6 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
+ const node_fs_1 = require("node:fs");
39
40
  const promises_1 = require("node:fs/promises");
40
41
  const node_path_1 = __importDefault(require("node:path"));
41
42
  const commander_1 = require("commander");
@@ -65,6 +66,14 @@ const EXAMPLES_TEXT = [
65
66
  ' $ wxgzh cover --title "我的文章" --to .wxgzh/cover.jpg',
66
67
  ' $ wxgzh publish --article .wxgzh/article.html --cover .wxgzh/cover.jpg'
67
68
  ].join('\n');
69
+ function getCliVersion() {
70
+ const packageJsonPath = node_path_1.default.resolve(__dirname, '../../package.json');
71
+ const packageJson = JSON.parse((0, node_fs_1.readFileSync)(packageJsonPath, 'utf8'));
72
+ if (typeof packageJson.version !== 'string' || packageJson.version.trim() === '') {
73
+ throw new Error('读取版本号失败: package.json 中缺少有效的 version 字段');
74
+ }
75
+ return packageJson.version;
76
+ }
68
77
  function resolveCoverValue(value) {
69
78
  return typeof value === 'string' ? value : undefined;
70
79
  }
@@ -123,6 +132,7 @@ async function main() {
123
132
  .description('把 Markdown 文章处理成微信公众号草稿,支持主题、图片修复、封面生成与一键发布')
124
133
  .usage('[article.md] [options]')
125
134
  .helpOption('-h, --help', '查看帮助')
135
+ .version(getCliVersion(), '-V, --version', '查看版本')
126
136
  .showHelpAfterError('(使用 --help 查看完整帮助)')
127
137
  .argument('[article]', 'Markdown 文件路径;传入后会自动执行 md2html -> fix -> cover -> publish')
128
138
  .option('--theme <theme>', `指定主题名,可用值:${(0, themes_1.listAvailableThemes)().join(', ')}`)
@@ -7,12 +7,17 @@ exports.listCoverPresets = listCoverPresets;
7
7
  exports.createCover = createCover;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
+ const opentype_js_1 = require("opentype.js");
10
11
  const sharp_1 = __importDefault(require("sharp"));
11
12
  const BUILTIN_BACKGROUND_DIR = node_path_1.default.resolve(__dirname, '../../assets/backgrounds');
13
+ const COVER_TITLE_FONT_PATH = node_path_1.default.resolve(__dirname, '../../assets/fonts/仓耳小丸子.ttf');
12
14
  const FALLBACK_GRADIENT = {
13
15
  start: '#e8f3ef',
14
16
  end: '#c9ded6'
15
17
  };
18
+ const DEFAULT_TITLE_LINE_LENGTH = 15;
19
+ const COVER_TEXT_COLOR = '#000000b8';
20
+ let cachedCoverFont;
16
21
  function escapeXml(value) {
17
22
  return value
18
23
  .replace(/&/g, '&amp;')
@@ -21,8 +26,16 @@ function escapeXml(value) {
21
26
  .replace(/"/g, '&quot;')
22
27
  .replace(/'/g, '&apos;');
23
28
  }
24
- function escapeAttribute(value) {
25
- return escapeXml(value);
29
+ function getCoverFont() {
30
+ if (!(0, node_fs_1.existsSync)(COVER_TITLE_FONT_PATH)) {
31
+ return undefined;
32
+ }
33
+ if (!cachedCoverFont) {
34
+ const fontBuffer = (0, node_fs_1.readFileSync)(COVER_TITLE_FONT_PATH);
35
+ const arrayBuffer = fontBuffer.buffer.slice(fontBuffer.byteOffset, fontBuffer.byteOffset + fontBuffer.byteLength);
36
+ cachedCoverFont = (0, opentype_js_1.parse)(arrayBuffer);
37
+ }
38
+ return cachedCoverFont;
26
39
  }
27
40
  function readFiles(dirPath, pattern) {
28
41
  try {
@@ -79,29 +92,66 @@ function buildGradientBackground(width, height) {
79
92
  </svg>`;
80
93
  return Buffer.from(svg);
81
94
  }
95
+ function getFontLineMetrics(font, fontSize) {
96
+ const unitsPerEm = font.unitsPerEm || 1000;
97
+ const ascender = Math.round(font.ascender / unitsPerEm * fontSize);
98
+ const descender = Math.abs(Math.round(font.descender / unitsPerEm * fontSize));
99
+ return {
100
+ ascender,
101
+ lineHeight: Math.max(Math.round((ascender + descender) * 1.08), fontSize)
102
+ };
103
+ }
104
+ function buildFontPath(font, text, fontSize) {
105
+ const pathObject = font.getPath(text, 0, 0, fontSize);
106
+ const box = pathObject.getBoundingBox();
107
+ return {
108
+ pathData: pathObject.toPathData(3),
109
+ width: Math.max(box.x2 - box.x1, 0),
110
+ x1: box.x1,
111
+ x2: box.x2
112
+ };
113
+ }
114
+ function buildPathElement(pathData, x, y) {
115
+ return `<path d="${pathData}" transform="translate(${x.toFixed(3)} ${y.toFixed(3)})" fill="${COVER_TEXT_COLOR}" />`;
116
+ }
117
+ function buildTitlePath(font, line, index, canvasWidth, titleTop, lineHeight, ascender, fontSize) {
118
+ const glyphPath = buildFontPath(font, line, fontSize);
119
+ const baselineY = titleTop + index * lineHeight + ascender;
120
+ const startX = (canvasWidth - glyphPath.width) / 2 - glyphPath.x1;
121
+ return {
122
+ right: startX + glyphPath.x2,
123
+ path: buildPathElement(glyphPath.pathData, startX, baselineY)
124
+ };
125
+ }
126
+ function buildAuthorPath(font, text, fontSize, right, baselineY) {
127
+ const glyphPath = buildFontPath(font, text, fontSize);
128
+ const startX = right - glyphPath.x2;
129
+ return buildPathElement(glyphPath.pathData, startX, baselineY);
130
+ }
82
131
  function buildTextSvg(params) {
83
- const lines = buildTitleLines(params.title, 10);
132
+ const font = getCoverFont();
133
+ if (!font) {
134
+ return undefined;
135
+ }
136
+ const lines = buildTitleLines(params.title, DEFAULT_TITLE_LINE_LENGTH);
84
137
  const sizes = estimateFontSize(params.width, lines);
85
- const lineHeight = Math.round(sizes.title * 1.15);
138
+ const titleMetrics = getFontLineMetrics(font, sizes.title);
139
+ const authorMetrics = getFontLineMetrics(font, sizes.author);
140
+ const lineHeight = titleMetrics.lineHeight;
86
141
  const titleBlockHeight = lineHeight * lines.length;
87
142
  const titleTop = Math.round((params.height - titleBlockHeight) / 2);
88
- const fontFamily = '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif';
89
- const titleWidth = sizes.longestChars * sizes.title;
90
- const titleRight = Math.round(params.width / 2 + titleWidth / 2);
91
- const authorY = titleTop + titleBlockHeight + sizes.author + Math.round(sizes.author * 0.4);
92
- const titleText = lines
93
- .map((line, index) => {
94
- const y = titleTop + sizes.title + index * lineHeight;
95
- return `<text x="50%" y="${y}" text-anchor="middle" font-size="${sizes.title}" font-family="${escapeAttribute(fontFamily)}" fill="rgba(0,0,0,0.72)">${escapeXml(line)}</text>`;
96
- })
97
- .join('');
98
- const authorText = params.author.trim()
99
- ? `<text x="${titleRight}" y="${authorY}" text-anchor="end" font-size="${sizes.author}" font-family="${escapeAttribute(fontFamily)}" fill="rgba(0,0,0,0.72)">${escapeXml(params.author.trim())}</text>`
143
+ const titlePaths = lines.map((line, index) => buildTitlePath(font, line, index, params.width, titleTop, lineHeight, titleMetrics.ascender, sizes.title));
144
+ const titleRight = titlePaths.length > 0
145
+ ? Math.max(...titlePaths.map((item) => item.right))
146
+ : params.width / 2;
147
+ const authorText = params.author.trim();
148
+ const authorPath = authorText
149
+ ? buildAuthorPath(font, authorText, sizes.author, titleRight, titleTop + titleBlockHeight + Math.round(sizes.author * 0.4) + authorMetrics.ascender)
100
150
  : '';
101
151
  const svg = `
102
152
  <svg width="${params.width}" height="${params.height}" viewBox="0 0 ${params.width} ${params.height}" xmlns="http://www.w3.org/2000/svg">
103
- ${titleText}
104
- ${authorText}
153
+ ${titlePaths.map((item) => item.path).join('')}
154
+ ${authorPath}
105
155
  </svg>`;
106
156
  return Buffer.from(svg);
107
157
  }
@@ -137,8 +187,13 @@ async function createCover(options) {
137
187
  const background = backgroundPath
138
188
  ? (0, sharp_1.default)(backgroundPath).resize(width, height, { fit: 'cover' })
139
189
  : (0, sharp_1.default)(buildGradientBackground(width, height));
140
- await background
141
- .composite([
190
+ const textSvg = buildTextSvg({
191
+ title: options.title,
192
+ author,
193
+ width,
194
+ height
195
+ });
196
+ const overlays = [
142
197
  {
143
198
  input: {
144
199
  create: {
@@ -148,16 +203,13 @@ async function createCover(options) {
148
203
  background: { r: 255, g: 255, b: 255, alpha: 0.58 }
149
204
  }
150
205
  }
151
- },
152
- {
153
- input: buildTextSvg({
154
- title: options.title,
155
- author,
156
- width,
157
- height
158
- })
159
206
  }
160
- ])
207
+ ];
208
+ if (textSvg) {
209
+ overlays.push({ input: textSvg });
210
+ }
211
+ await background
212
+ .composite(overlays)
161
213
  .jpeg({ quality: 92 })
162
214
  .toFile(outputPath);
163
215
  return outputPath;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyhue1991/wxgzh",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "Markdown to WeChat Official Account draft CLI",
5
5
  "license": "MIT",
6
6
  "main": "dist/cli/index.js",
@@ -37,6 +37,7 @@
37
37
  "markdown-it": "^14.1.0",
38
38
  "markdown-it-mathjax3": "^5.2.0",
39
39
  "mathjax": "^4.1.1",
40
+ "opentype.js": "^1.3.4",
40
41
  "sharp": "^0.33.5"
41
42
  },
42
43
  "devDependencies": {
package/spec.md CHANGED
@@ -482,6 +482,7 @@ POST https://api.weixin.qq.com/cgi-bin/draft/add
482
482
  npm run typecheck
483
483
  npm run build
484
484
  node dist/cli/index.js --help
485
+ node dist/cli/index.js --version
485
486
  node dist/cli/index.js config --help
486
487
  node dist/cli/index.js md2html --help
487
488
  node dist/cli/index.js fix --help