fast-cocos 1.0.8 → 1.1.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/copy-assets.json +8 -0
- package/dist/audio/src/audio.js +7 -0
- package/dist/audio/src/index.js +4 -0
- package/dist/bmfont/src/BMFont.js +9 -0
- package/dist/bmfont/src/index.js +4 -0
- package/dist/index.js +24 -1
- package/dist/json/src/JsonUtils.js +50 -0
- package/dist/json/src/index.js +10 -7
- package/dist/logger.js +21 -0
- package/dist/robot/src/Robot.js +9 -3
- package/dist/robot/src/index.js +4 -0
- package/dist/ttf/jar/font.jar +0 -0
- package/dist/ttf/jar/sfnttool.jar +0 -0
- package/dist/ttf/py/modify_fonts.py +251 -0
- package/dist/ttf/py/modify_fonts1.py +284 -0
- package/dist/ttf/src/TTF.js +83 -0
- package/dist/ttf/src/index.js +34 -0
- package/dist/utils.js +4 -0
- package/dist/xlsx/src/XlsxParser.js +26 -4
- package/dist/xlsx/src/index.js +4 -0
- package/dist/xlsx/template/xlsx.txt +1 -3
- package/dist/xxteakey/src/index.js +4 -0
- package/package.json +1 -1
- package/dist/json/src/JSON.js +0 -111
package/copy-assets.json
CHANGED
package/dist/audio/src/audio.js
CHANGED
|
@@ -10,6 +10,10 @@ const xlsx_1 = __importDefault(require("xlsx"));
|
|
|
10
10
|
const logger_1 = __importDefault(require("../../logger"));
|
|
11
11
|
const utils_1 = require("../../utils");
|
|
12
12
|
class Audio {
|
|
13
|
+
/**
|
|
14
|
+
* 根据音频配置表复制 mp3 文件,并生成对应的 TypeScript 引用配置。
|
|
15
|
+
* @param options 音频导出选项
|
|
16
|
+
*/
|
|
13
17
|
parse(options) {
|
|
14
18
|
const files = utils_1.utils.findFiles(options.source, [".xlsx"]);
|
|
15
19
|
for (const filePath of files) {
|
|
@@ -27,6 +31,7 @@ class Audio {
|
|
|
27
31
|
const fileName = xlsxName.charAt(0).toUpperCase() + xlsxName.slice(1) + "Audio";
|
|
28
32
|
let str = `export const ${fileName} = {`;
|
|
29
33
|
const lastIndex = json.length - 1;
|
|
34
|
+
// 将每一行配置转换成资源复制动作和 TS 映射项。
|
|
30
35
|
json.forEach((item, index) => {
|
|
31
36
|
const fileName = item["文件名"];
|
|
32
37
|
const refName = item["引用名"];
|
|
@@ -59,7 +64,9 @@ class Audio {
|
|
|
59
64
|
utils_1.utils.writeFileSync(tsFilePath, str);
|
|
60
65
|
// 删除未使用mp3文件
|
|
61
66
|
oldMp3
|
|
67
|
+
// 找出本次导出结果中已经不存在的旧音频文件。
|
|
62
68
|
.filter((v) => !newMp3.includes(v))
|
|
69
|
+
// 同步删除音频文件及其 Cocos meta 文件。
|
|
63
70
|
.forEach((v) => {
|
|
64
71
|
utils_1.utils.rmSync(v);
|
|
65
72
|
utils_1.utils.rmSync(v + ".meta");
|
package/dist/audio/src/index.js
CHANGED
|
@@ -11,6 +11,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const commander_1 = require("commander");
|
|
13
13
|
const audio_1 = require("./audio");
|
|
14
|
+
/**
|
|
15
|
+
* 解析音频导出命令参数,并启动音频配置生成流程。
|
|
16
|
+
*/
|
|
14
17
|
function main() {
|
|
15
18
|
return __awaiter(this, void 0, void 0, function* () {
|
|
16
19
|
commander_1.program
|
|
@@ -31,6 +34,7 @@ function main() {
|
|
|
31
34
|
});
|
|
32
35
|
});
|
|
33
36
|
}
|
|
37
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
34
38
|
void main().catch((err) => {
|
|
35
39
|
console.error(err);
|
|
36
40
|
process.exit(1);
|
|
@@ -21,10 +21,17 @@ class BMFont {
|
|
|
21
21
|
// 所有字体目录
|
|
22
22
|
utils_1.utils
|
|
23
23
|
.getSubDirsSync(dir)
|
|
24
|
+
// 只处理包含 png 字符图片的字体目录。
|
|
24
25
|
.filter((dir) => utils_1.utils.containsFileExt(dir, ".png"))
|
|
26
|
+
// 为每个字体目录生成对应的 png 图集和 fnt 描述文件。
|
|
25
27
|
.forEach((fontDir) => this.run(fontDir, outputDir));
|
|
26
28
|
}
|
|
27
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* 将单个字体目录中的字符图片打包成 BMFont 文件。
|
|
32
|
+
* @param fontDir 字符图片所在目录
|
|
33
|
+
* @param outputDir 输出目录
|
|
34
|
+
*/
|
|
28
35
|
run(fontDir, outputDir) {
|
|
29
36
|
const fontName = path_1.default.basename(fontDir);
|
|
30
37
|
const pngPath = path_1.default.join(outputDir, `${fontName}.png`);
|
|
@@ -36,6 +43,7 @@ class BMFont {
|
|
|
36
43
|
},
|
|
37
44
|
algorithm: "left-right"
|
|
38
45
|
};
|
|
46
|
+
// SpriteSmith 完成图集生成后,同步写入图片和 fnt 字符映射。
|
|
39
47
|
spritesmith_1.default.run(config, (err, result) => {
|
|
40
48
|
if (err) {
|
|
41
49
|
console.log(err);
|
|
@@ -102,6 +110,7 @@ class BMFont {
|
|
|
102
110
|
}
|
|
103
111
|
a[i] = code;
|
|
104
112
|
}
|
|
113
|
+
// 按十六进制位权累计每一位数值。
|
|
105
114
|
return a.reduce(function (acc, c) {
|
|
106
115
|
acc = 16 * acc + c;
|
|
107
116
|
return acc;
|
package/dist/bmfont/src/index.js
CHANGED
|
@@ -11,6 +11,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const commander_1 = require("commander");
|
|
13
13
|
const BMFont_1 = require("./BMFont");
|
|
14
|
+
/**
|
|
15
|
+
* 解析 BMFont 导出命令参数,并启动字体图集生成流程。
|
|
16
|
+
*/
|
|
14
17
|
function main() {
|
|
15
18
|
return __awaiter(this, void 0, void 0, function* () {
|
|
16
19
|
commander_1.program
|
|
@@ -27,6 +30,7 @@ function main() {
|
|
|
27
30
|
});
|
|
28
31
|
});
|
|
29
32
|
}
|
|
33
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
30
34
|
void main().catch((err) => {
|
|
31
35
|
console.error(err);
|
|
32
36
|
process.exit(1);
|
package/dist/index.js
CHANGED
|
@@ -15,24 +15,44 @@ fast-cocos audio export audio config spreadsheet
|
|
|
15
15
|
fast-cocos ttf export ttf fonts
|
|
16
16
|
fast-cocos robot send dingtalk / feishu (lark) messages
|
|
17
17
|
fast-cocos bmfont export bmfont fonts
|
|
18
|
-
fast-cocos xxteakey generate xxteakey
|
|
18
|
+
fast-cocos xxteakey generate xxteakey
|
|
19
|
+
fast-cocos json json operations`;
|
|
20
|
+
/**
|
|
21
|
+
* 打印 CLI 的命令使用说明。
|
|
22
|
+
*/
|
|
19
23
|
function printUsage() {
|
|
20
24
|
console.info(USAGE);
|
|
21
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 从包信息中读取并打印当前工具版本。
|
|
28
|
+
*/
|
|
22
29
|
function printVersion() {
|
|
23
30
|
const packageJsonPath = path_1.default.join(__dirname, "..", "package.json");
|
|
24
31
|
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf8"));
|
|
25
32
|
console.info(packageJson.version);
|
|
26
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* 输出未知命令提示,引导用户查看帮助。
|
|
36
|
+
* @param cmd 用户输入的命令名
|
|
37
|
+
*/
|
|
27
38
|
function printUnknownCommandHint(cmd) {
|
|
28
39
|
console.error(`Unknown command: "${cmd}"`);
|
|
29
40
|
console.error("");
|
|
30
41
|
console.error("To see a list of supported fast-cocos commands, run:");
|
|
31
42
|
console.error("fast-cocos help");
|
|
32
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* 判断子命令编译后的入口文件是否存在。
|
|
46
|
+
* @param absPathWithoutExt 不带扩展名的绝对路径
|
|
47
|
+
*/
|
|
33
48
|
function commandModuleJsExists(absPathWithoutExt) {
|
|
34
49
|
return fs_1.default.existsSync(`${absPathWithoutExt}.js`);
|
|
35
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* 校验并加载子命令入口模块。
|
|
53
|
+
* @param cmd 用户输入的命令名
|
|
54
|
+
* @param absPathWithoutExt 不带扩展名的子命令入口绝对路径
|
|
55
|
+
*/
|
|
36
56
|
function requireCommand(cmd, absPathWithoutExt) {
|
|
37
57
|
if (!commandModuleJsExists(absPathWithoutExt)) {
|
|
38
58
|
printUnknownCommandHint(cmd);
|
|
@@ -40,6 +60,9 @@ function requireCommand(cmd, absPathWithoutExt) {
|
|
|
40
60
|
}
|
|
41
61
|
require(absPathWithoutExt);
|
|
42
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* 解析根命令参数,并将执行权交给对应子命令模块。
|
|
65
|
+
*/
|
|
43
66
|
function main() {
|
|
44
67
|
const [, , cmd] = process.argv;
|
|
45
68
|
if (!cmd || cmd === "-h" || cmd === "--help" || cmd === "help") {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.jsonUtils = void 0;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const utils_1 = require("../../utils");
|
|
9
|
+
const DEFAULT_MERGE_FILE = "merge.json";
|
|
10
|
+
class JsonUtils {
|
|
11
|
+
/**
|
|
12
|
+
* 合并 JSON 文件并写入输出目录。
|
|
13
|
+
* @param options JSON 合并选项
|
|
14
|
+
*/
|
|
15
|
+
merge(options) {
|
|
16
|
+
utils_1.utils.writeJson(this.getOutputPath(options), this.mergeJsonFiles(options));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 获取合并结果的输出路径。
|
|
20
|
+
* @param options JSON 合并选项
|
|
21
|
+
*/
|
|
22
|
+
getOutputPath(options) {
|
|
23
|
+
return path_1.default.join(options.output, options.file || DEFAULT_MERGE_FILE);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 从 JSON 文件路径中提取合并后的对象 key。
|
|
27
|
+
* @param filePath JSON 文件路径
|
|
28
|
+
*/
|
|
29
|
+
getJsonFileKey(filePath) {
|
|
30
|
+
return path_1.default.basename(filePath, path_1.default.extname(filePath));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 递归读取目录中的 JSON 文件,并按文件名合并为一个对象。
|
|
34
|
+
* @param options JSON 合并选项
|
|
35
|
+
*/
|
|
36
|
+
mergeJsonFiles(options) {
|
|
37
|
+
const outputPath = this.getOutputPath(options);
|
|
38
|
+
const sourceFiles = utils_1.utils.findFiles(options.source, [".json"]);
|
|
39
|
+
const merged = {};
|
|
40
|
+
// 将每个 JSON 文件内容挂到对应文件名 key 下。
|
|
41
|
+
sourceFiles
|
|
42
|
+
.filter((filePath) => path_1.default.resolve(filePath) !== path_1.default.resolve(outputPath))
|
|
43
|
+
.forEach((filePath) => {
|
|
44
|
+
const json = utils_1.utils.readJson(filePath);
|
|
45
|
+
merged[this.getJsonFileKey(filePath)] = json;
|
|
46
|
+
});
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.jsonUtils = new JsonUtils();
|
package/dist/json/src/index.js
CHANGED
|
@@ -10,23 +10,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const commander_1 = require("commander");
|
|
13
|
-
const
|
|
13
|
+
const JsonUtils_1 = require("./JsonUtils");
|
|
14
|
+
/**
|
|
15
|
+
* 解析 JSON 合并命令参数,并将目录中的 JSON 文件合并输出。
|
|
16
|
+
*/
|
|
14
17
|
function main() {
|
|
15
18
|
return __awaiter(this, void 0, void 0, function* () {
|
|
16
19
|
commander_1.program
|
|
17
|
-
.name("
|
|
18
|
-
.description("
|
|
20
|
+
.name("merge-json")
|
|
21
|
+
.description("merge json utility")
|
|
19
22
|
.version("1.0.0")
|
|
20
23
|
.requiredOption("-s, --source <source>", "source directory")
|
|
21
24
|
.requiredOption("-o, --output <output>", "output directory")
|
|
25
|
+
.option("-m, --merge <merge>")
|
|
26
|
+
.option("-f, --file <file name>", "file name")
|
|
22
27
|
.parse(process.argv);
|
|
23
28
|
const options = commander_1.program.opts();
|
|
24
|
-
|
|
25
|
-
source: options.source,
|
|
26
|
-
output: options.output
|
|
27
|
-
});
|
|
29
|
+
JsonUtils_1.jsonUtils.merge(options);
|
|
28
30
|
});
|
|
29
31
|
}
|
|
32
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
30
33
|
void main().catch((err) => {
|
|
31
34
|
console.error(err);
|
|
32
35
|
process.exit(1);
|
package/dist/logger.js
CHANGED
|
@@ -8,24 +8,45 @@
|
|
|
8
8
|
*/
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
class Logger {
|
|
11
|
+
/**
|
|
12
|
+
* 以默认颜色输出普通日志。
|
|
13
|
+
* @param args 日志内容
|
|
14
|
+
*/
|
|
11
15
|
log(...args) {
|
|
12
16
|
console.log("\x1b[37m%s\x1b[0m", this.format(...args));
|
|
13
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* 以红色输出错误日志。
|
|
20
|
+
* @param args 日志内容
|
|
21
|
+
*/
|
|
14
22
|
error(...args) {
|
|
15
23
|
console.log("\x1b[31m%s\x1b[0m", this.format(...args));
|
|
16
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* 以黄色输出警告日志。
|
|
27
|
+
* @param args 日志内容
|
|
28
|
+
*/
|
|
17
29
|
warn(...args) {
|
|
18
30
|
console.log("\x1b[33m%s\x1b[0m", this.format(...args));
|
|
19
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* 以绿色输出提示日志。
|
|
34
|
+
* @param args 日志内容
|
|
35
|
+
*/
|
|
20
36
|
info(...args) {
|
|
21
37
|
console.log("\x1b[32m%s\x1b[0m", this.format(...args));
|
|
22
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* 将任意日志参数格式化为带时间前缀的字符串。
|
|
41
|
+
* @param args 日志内容
|
|
42
|
+
*/
|
|
23
43
|
format(...args) {
|
|
24
44
|
let preStr = "";
|
|
25
45
|
while (args[0] == "\n") {
|
|
26
46
|
preStr += args.shift();
|
|
27
47
|
}
|
|
28
48
|
let message = "";
|
|
49
|
+
// 将数组、对象和基础类型统一转成可读文本。
|
|
29
50
|
args.forEach((arg) => {
|
|
30
51
|
if (Array.isArray(arg)) {
|
|
31
52
|
message += JSON.stringify(arg) + " ";
|
package/dist/robot/src/Robot.js
CHANGED
|
@@ -13,6 +13,7 @@ class Robot {
|
|
|
13
13
|
* @param content 内容
|
|
14
14
|
*/
|
|
15
15
|
sendToDingTalk(url, content) {
|
|
16
|
+
// 包装为 Promise,保证调用方可以等待请求完成或失败处理结束。
|
|
16
17
|
return new Promise((resolve) => {
|
|
17
18
|
if (!url) {
|
|
18
19
|
logger_1.default.warn("未配置钉钉机器人地址");
|
|
@@ -43,7 +44,9 @@ class Robot {
|
|
|
43
44
|
// 创建并发送请求
|
|
44
45
|
const req = https_1.default.request(options, (res) => {
|
|
45
46
|
let data = "";
|
|
47
|
+
// 累积响应体,便于日志中输出机器人接口返回内容。
|
|
46
48
|
res.on("data", (chunk) => (data += chunk));
|
|
49
|
+
// 响应结束后记录结果并释放等待方。
|
|
47
50
|
res.on("end", () => {
|
|
48
51
|
logger_1.default.log("成功发送到钉钉:", data);
|
|
49
52
|
resolve();
|
|
@@ -60,11 +63,12 @@ class Robot {
|
|
|
60
63
|
});
|
|
61
64
|
}
|
|
62
65
|
/**
|
|
63
|
-
*
|
|
64
|
-
* @param url
|
|
66
|
+
* 发送内容到飞书
|
|
67
|
+
* @param url 飞书机器人地址
|
|
65
68
|
* @param text 内容
|
|
66
69
|
*/
|
|
67
70
|
sendToFeiShu(url, text) {
|
|
71
|
+
// 包装为 Promise,保证调用方可以等待请求完成或失败处理结束。
|
|
68
72
|
return new Promise((resolve) => {
|
|
69
73
|
if (!url) {
|
|
70
74
|
logger_1.default.warn("未配置飞书机器人地址");
|
|
@@ -74,7 +78,7 @@ class Robot {
|
|
|
74
78
|
logger_1.default.info("发送飞书消息:");
|
|
75
79
|
logger_1.default.info(url);
|
|
76
80
|
logger_1.default.info(text);
|
|
77
|
-
//
|
|
81
|
+
// 发送的数据,飞书机器人使用 msg_type 和 content 描述消息体
|
|
78
82
|
const message = JSON.stringify({
|
|
79
83
|
msg_type: "text",
|
|
80
84
|
content: {
|
|
@@ -99,7 +103,9 @@ class Robot {
|
|
|
99
103
|
// 创建并发送请求
|
|
100
104
|
const req = https_1.default.request(options, (res) => {
|
|
101
105
|
let data = "";
|
|
106
|
+
// 累积响应体,便于日志中输出机器人接口返回内容。
|
|
102
107
|
res.on("data", (chunk) => (data += chunk));
|
|
108
|
+
// 响应结束后记录结果并释放等待方。
|
|
103
109
|
res.on("end", () => {
|
|
104
110
|
logger_1.default.log("成功发送到飞书:", data);
|
|
105
111
|
resolve();
|
package/dist/robot/src/index.js
CHANGED
|
@@ -11,6 +11,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
const commander_1 = require("commander");
|
|
13
13
|
const Robot_1 = require("./Robot");
|
|
14
|
+
/**
|
|
15
|
+
* 解析机器人消息命令参数,并按类型发送到对应平台。
|
|
16
|
+
*/
|
|
14
17
|
function main() {
|
|
15
18
|
return __awaiter(this, void 0, void 0, function* () {
|
|
16
19
|
commander_1.program
|
|
@@ -34,6 +37,7 @@ function main() {
|
|
|
34
37
|
}
|
|
35
38
|
});
|
|
36
39
|
}
|
|
40
|
+
// 捕获入口异步异常并以非零状态码退出。
|
|
37
41
|
void main().catch((err) => {
|
|
38
42
|
console.error(err);
|
|
39
43
|
process.exit(1);
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from fontTools.ttLib import TTFont
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
def get_font_info(font_path):
|
|
8
|
+
"""获取字体的原始信息"""
|
|
9
|
+
font = TTFont(font_path)
|
|
10
|
+
name_table = font['name']
|
|
11
|
+
|
|
12
|
+
info = {
|
|
13
|
+
'family': None,
|
|
14
|
+
'subfamily': None,
|
|
15
|
+
'fullname': None,
|
|
16
|
+
'postscript': None,
|
|
17
|
+
'weight': 400
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# 获取字体信息
|
|
21
|
+
for record in name_table.names:
|
|
22
|
+
if record.nameID == 1: # Family name
|
|
23
|
+
info['family'] = record.toUnicode()
|
|
24
|
+
elif record.nameID == 2: # Subfamily
|
|
25
|
+
info['subfamily'] = record.toUnicode()
|
|
26
|
+
elif record.nameID == 4: # Full name
|
|
27
|
+
info['fullname'] = record.toUnicode()
|
|
28
|
+
elif record.nameID == 6: # PostScript name
|
|
29
|
+
info['postscript'] = record.toUnicode()
|
|
30
|
+
|
|
31
|
+
# 获取字重
|
|
32
|
+
if 'OS/2' in font:
|
|
33
|
+
info['weight'] = font['OS/2'].usWeightClass
|
|
34
|
+
|
|
35
|
+
font.close()
|
|
36
|
+
return info
|
|
37
|
+
|
|
38
|
+
def detect_weight_from_font(font_path):
|
|
39
|
+
"""从字体文件的OS/2表获取实际字重"""
|
|
40
|
+
font = TTFont(font_path)
|
|
41
|
+
weight = 400 # 默认
|
|
42
|
+
|
|
43
|
+
if 'OS/2' in font:
|
|
44
|
+
weight = font['OS/2'].usWeightClass
|
|
45
|
+
|
|
46
|
+
font.close()
|
|
47
|
+
|
|
48
|
+
# 根据字重值返回对应的样式名
|
|
49
|
+
if weight <= 100:
|
|
50
|
+
return weight, 'Thin'
|
|
51
|
+
elif weight <= 200:
|
|
52
|
+
return weight, 'ExtraLight'
|
|
53
|
+
elif weight <= 300:
|
|
54
|
+
return weight, 'Light'
|
|
55
|
+
elif weight <= 400:
|
|
56
|
+
return weight, 'Regular'
|
|
57
|
+
elif weight <= 500:
|
|
58
|
+
return weight, 'Medium'
|
|
59
|
+
elif weight <= 600:
|
|
60
|
+
return weight, 'SemiBold'
|
|
61
|
+
elif weight <= 700:
|
|
62
|
+
return weight, 'Bold'
|
|
63
|
+
elif weight <= 800:
|
|
64
|
+
return weight, 'ExtraBold'
|
|
65
|
+
else:
|
|
66
|
+
return weight, 'Black'
|
|
67
|
+
|
|
68
|
+
def generate_unique_family_name(original_family, weight_style, index):
|
|
69
|
+
"""基于索引生成唯一的字体家族名"""
|
|
70
|
+
# 移除原始家族名中的非ASCII字符和特殊字符
|
|
71
|
+
clean_name = re.sub(r'[^\w]', '', original_family)
|
|
72
|
+
clean_name = ''.join(c for c in clean_name if ord(c) < 128) # 只保留ASCII字符
|
|
73
|
+
|
|
74
|
+
# 如果清理后为空,使用默认名称
|
|
75
|
+
if not clean_name:
|
|
76
|
+
clean_name = "Font"
|
|
77
|
+
|
|
78
|
+
# 生成简短的唯一标识
|
|
79
|
+
if len(clean_name) > 8:
|
|
80
|
+
clean_name = clean_name[:8]
|
|
81
|
+
|
|
82
|
+
# 基于索引和字重生成唯一名称
|
|
83
|
+
return f"{clean_name}{weight_style}{index}"
|
|
84
|
+
|
|
85
|
+
def modify_font_complete(font_path, output_dir=None, new_family_name=None, weight_value=None, style_name=None, index=1):
|
|
86
|
+
"""完全修改字体的所有相关元数据"""
|
|
87
|
+
# 如果没有提供参数,自动检测
|
|
88
|
+
if not all([new_family_name, weight_value, style_name]):
|
|
89
|
+
font_info = get_font_info(font_path)
|
|
90
|
+
|
|
91
|
+
# 从字体文件获取实际字重
|
|
92
|
+
detected_weight, detected_style = detect_weight_from_font(font_path)
|
|
93
|
+
|
|
94
|
+
# 如果没有提供参数,使用检测到的值
|
|
95
|
+
if not new_family_name:
|
|
96
|
+
new_family_name = generate_unique_family_name(font_info['family'], detected_style, index)
|
|
97
|
+
if not weight_value:
|
|
98
|
+
weight_value = detected_weight
|
|
99
|
+
if not style_name:
|
|
100
|
+
style_name = "Regular" # iOS推荐所有独立字体都使用Regular
|
|
101
|
+
|
|
102
|
+
print(f"\n处理字体: {os.path.basename(font_path)}")
|
|
103
|
+
print(f"新家族名: {new_family_name}")
|
|
104
|
+
print(f"字重: {weight_value}")
|
|
105
|
+
print(f"样式: {style_name}")
|
|
106
|
+
|
|
107
|
+
font = TTFont(font_path)
|
|
108
|
+
|
|
109
|
+
# 获取name表
|
|
110
|
+
name_table = font['name']
|
|
111
|
+
|
|
112
|
+
# 需要修改的所有nameID
|
|
113
|
+
# 1 = Family name
|
|
114
|
+
# 2 = Subfamily name
|
|
115
|
+
# 3 = Unique identifier
|
|
116
|
+
# 4 = Full name
|
|
117
|
+
# 6 = PostScript name
|
|
118
|
+
# 16 = Typographic Family name (Preferred family)
|
|
119
|
+
# 17 = Typographic Subfamily name (Preferred subfamily)
|
|
120
|
+
|
|
121
|
+
postscript_name = f"{new_family_name.replace(' ', '')}-{style_name.replace(' ', '')}"
|
|
122
|
+
full_name = f"{new_family_name} {style_name}"
|
|
123
|
+
|
|
124
|
+
# 创建新的name记录
|
|
125
|
+
name_updates = {
|
|
126
|
+
1: new_family_name, # Family
|
|
127
|
+
2: style_name, # Subfamily
|
|
128
|
+
3: f"{new_family_name} {style_name}", # Unique ID
|
|
129
|
+
4: full_name, # Full name
|
|
130
|
+
6: postscript_name, # PostScript name
|
|
131
|
+
16: new_family_name, # Preferred Family
|
|
132
|
+
17: style_name, # Preferred Subfamily
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# 更新所有相关的name记录
|
|
136
|
+
for record in name_table.names:
|
|
137
|
+
if record.nameID in name_updates:
|
|
138
|
+
old_value = record.toUnicode()
|
|
139
|
+
new_value = name_updates[record.nameID]
|
|
140
|
+
try:
|
|
141
|
+
if record.platformID == 3: # Windows
|
|
142
|
+
record.string = new_value.encode('utf-16-be')
|
|
143
|
+
elif record.platformID == 1: # Mac
|
|
144
|
+
record.string = new_value.encode('mac_roman')
|
|
145
|
+
else:
|
|
146
|
+
record.string = new_value.encode('utf-8')
|
|
147
|
+
print(f"NameID {record.nameID}: {old_value} -> {new_value}")
|
|
148
|
+
except:
|
|
149
|
+
# 如果编码失败,尝试使用原始编码
|
|
150
|
+
record.string = new_value.encode(record.getEncoding())
|
|
151
|
+
|
|
152
|
+
# 修改OS/2表
|
|
153
|
+
if 'OS/2' in font:
|
|
154
|
+
os2_table = font['OS/2']
|
|
155
|
+
old_weight = os2_table.usWeightClass
|
|
156
|
+
os2_table.usWeightClass = weight_value
|
|
157
|
+
print(f"OS/2 Weight: {old_weight} -> {weight_value}")
|
|
158
|
+
|
|
159
|
+
# 设置字体选择标志
|
|
160
|
+
os2_table.fsSelection = 0
|
|
161
|
+
if weight_value == 700: # Bold
|
|
162
|
+
os2_table.fsSelection |= 0x20 # Bold flag
|
|
163
|
+
elif weight_value == 400: # Regular
|
|
164
|
+
os2_table.fsSelection |= 0x40 # Regular flag
|
|
165
|
+
|
|
166
|
+
# 修改head表中的macStyle
|
|
167
|
+
if 'head' in font:
|
|
168
|
+
head_table = font['head']
|
|
169
|
+
head_table.macStyle = 0
|
|
170
|
+
if weight_value >= 700:
|
|
171
|
+
head_table.macStyle |= 0x01 # Bold
|
|
172
|
+
|
|
173
|
+
# 保存修改后的字体到out文件夹
|
|
174
|
+
if output_dir is None:
|
|
175
|
+
# 如果没有指定输出目录,使用脚本所在目录下的out文件夹
|
|
176
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
177
|
+
output_dir = os.path.join(script_dir, 'out')
|
|
178
|
+
|
|
179
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
180
|
+
|
|
181
|
+
font_basename = os.path.basename(font_path)
|
|
182
|
+
output_path = os.path.join(output_dir, font_basename)
|
|
183
|
+
font.save(output_path)
|
|
184
|
+
print(f"Saved to: {output_path}\n")
|
|
185
|
+
font.close()
|
|
186
|
+
|
|
187
|
+
return output_path
|
|
188
|
+
|
|
189
|
+
def find_font_files(font_dir):
|
|
190
|
+
"""查找指定目录下的所有TTF字体文件"""
|
|
191
|
+
font_files = []
|
|
192
|
+
if not os.path.exists(font_dir):
|
|
193
|
+
print(f"错误: 目录不存在: {font_dir}")
|
|
194
|
+
return font_files
|
|
195
|
+
|
|
196
|
+
for filename in os.listdir(font_dir):
|
|
197
|
+
if filename.lower().endswith('.ttf'):
|
|
198
|
+
font_path = os.path.join(font_dir, filename)
|
|
199
|
+
font_files.append(font_path)
|
|
200
|
+
|
|
201
|
+
return sorted(font_files)
|
|
202
|
+
|
|
203
|
+
def main():
|
|
204
|
+
# 获取脚本所在目录
|
|
205
|
+
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
206
|
+
font_dir = os.path.join(script_dir, 'font')
|
|
207
|
+
|
|
208
|
+
# 如果提供了命令行参数,使用命令行参数;否则自动扫描font文件夹
|
|
209
|
+
if len(sys.argv) >= 2:
|
|
210
|
+
font_files = []
|
|
211
|
+
for arg in sys.argv[1:]:
|
|
212
|
+
if os.path.isfile(arg) and arg.lower().endswith('.ttf'):
|
|
213
|
+
font_files.append(arg)
|
|
214
|
+
elif os.path.isdir(arg):
|
|
215
|
+
font_files.extend(find_font_files(arg))
|
|
216
|
+
else:
|
|
217
|
+
print(f"跳过无效路径: {arg}")
|
|
218
|
+
else:
|
|
219
|
+
# 自动扫描font文件夹
|
|
220
|
+
print(f"自动扫描字体文件夹: {font_dir}")
|
|
221
|
+
font_files = find_font_files(font_dir)
|
|
222
|
+
|
|
223
|
+
if not font_files:
|
|
224
|
+
print("未找到任何TTF字体文件")
|
|
225
|
+
print("\n用法:")
|
|
226
|
+
print(" python modify_fonts_complete.py # 自动处理font文件夹下的所有字体")
|
|
227
|
+
print(" python modify_fonts_complete.py <字体文件1> ... # 处理指定的字体文件")
|
|
228
|
+
print(" python modify_fonts_complete.py <文件夹路径> # 处理指定文件夹下的所有字体")
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
|
|
231
|
+
print(f"\n找到 {len(font_files)} 个字体文件,开始处理...\n")
|
|
232
|
+
|
|
233
|
+
# 处理所有字体文件
|
|
234
|
+
success_count = 0
|
|
235
|
+
error_count = 0
|
|
236
|
+
|
|
237
|
+
for index, font_path in enumerate(font_files, 1):
|
|
238
|
+
try:
|
|
239
|
+
modify_font_complete(font_path, index=index)
|
|
240
|
+
success_count += 1
|
|
241
|
+
except Exception as e:
|
|
242
|
+
print(f"处理 {font_path} 时出错: {e}\n")
|
|
243
|
+
error_count += 1
|
|
244
|
+
|
|
245
|
+
# 输出处理结果统计
|
|
246
|
+
print("=" * 50)
|
|
247
|
+
print(f"处理完成: 成功 {success_count} 个, 失败 {error_count} 个")
|
|
248
|
+
print("=" * 50)
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
main()
|