办公技术分享
如何把软件源码批量打印成 pdf 文件?升级版
申请软件著作权登记时,把源码批量生成成pdf文件的思路和实现
2025/7/21
申请软件著作权登记时,我们需要按相关的要求把代码打印成 pdf 文件上传。
上一个版本,我们将源码读取出来生成 html 文件,用 highlight.js
在网页上高亮显示出来后,使用浏览器打印功能生成单个 pdf 文件,使用 puppeteer-core
依次打开 html
文件,转换成带有每页固定行数,有行号、语法高亮的 pdf
文件。使用 pdf-merger-js
把多个 pdf
文件按顺序合并起来生成一份代码 pdf
文件。使用 pdf-lib
再合并多个 pdf,再打印上页眉标记。这个过程操作比较复杂。
我们现在重新思考打印源码这件事,就是制作一个文档,把代码当成文字,加上行号、语法高亮、页眉。那么我们用制作文档的思路来做。用制作文档的专业工具 Typst https://typst.app/
。
Typst 可以读取文件作为内容,可以使用 codly
包对代码进行高亮,默认的页眉、页码这些都支持。
总体流程
通过这个软件,我们制作源码 pdf 的思路就变成这样:
- 准备一个
typ
模板文件,设置好页面、页边距、页眉、页码等。 - 将源码文件读取到
main.typ
文件中,设置好代码框的参数,字体、字号、标题。 - 使用
typst compile <*.typ>
命令生成 pdf 文件。
一步搞定简直轻松愉快,不用合并 pdf 文件了,不用再次打页眉标记了。
提示
软件著作权登记上传源代码的要求:
页眉建议标注该软件名称、版本号,内容应与申请表中填写的一致;右上角标注页码,源程序从正文第 1 页编到第 60 页,文档从目录开始由第 1 页编到第 60 页。
使用的技术栈
cloc
统计源码行数
typst
排版软件
具体实现
获取要打印的代码文件
此步骤常见方法是通过 git 管理的文件中获取。
在源码目录运行
计算代码行数
sh
cloc $(git ls-files)
或者
sh
cloc <src_dir>
获取 git 仓库的文件清单
sh
git ls-files > filelist.txt
生成 filelist.txt
文件后,打开它,清除不需要的图片、压缩包 zip/xlsx/png/docx/.gitignore/cache
等二进制文件。
调整文件的排列顺序为自己需要的。
生成typ文件
下面是 typ 模板文件。里面使用占位符 {{title}} {{filelist}}
作为后续替换的占位符。
typ
#import "@preview/codly:1.3.0": *
#import "@preview/codly-languages:0.1.1": *
#set page(
paper: "a4",
margin: (
top: 1.5cm,
bottom: 1cm,
left: 1.5cm,
right: 1.5cm,
),
header: context [
#set align(right)
#set text(12pt)
{{title}}
#set text(8pt)
#h(1fr) 页码:
#counter(page).display(
"1 / 1",
both: true,
)
],
)
#set text(font: (
(name: "Noto Sans Mono", covers: "latin-in-cjk"),
"Noto Sans SC",
))
#show raw: set text(font: (
(name: "Noto Sans Mono", covers: "latin-in-cjk"),
"Noto Sans SC",
))
#show: codly-init.with()
#let files = ({{filelist}})
#let color = rgb("#646cff")
#for f in files {
codly(
zebra-fill: none,
display-icon: false,
header: raw(f),
header-repeat: true,
header-cell-args: (align: left),
header-transform: x => {
set text(fill: color)
strong(x)
line(length: 100%, stroke: 1pt + color)
},
languages: codly-languages,
lang-format: (_, _, _) => [],
number-align: right + top,
)
let lang = f.split(".").at(-1)
let text = read(f)
raw(text, block: true, lang: lang)
pagebreak()
}
我们读取 filelist.txt
文件,组合好路径好,直接替换模板中文件列表的占位符,typst 在生成时就会自动读取源码文件。
生成pdf文件
编译 main.typ
文件生成 main.pdf
文件。
sh
typst compile main.typ
看看代码文件生成的效果。每页 50 行代码,页眉标注该软件名称、版本号,右上角有页码。
实现的源码
js
const fs = require('fs');
const path = require('path');
function readLinesToArray(filePath, codePath) {
const data = fs.readFileSync(filePath, 'utf-8');
return data.split(/\r?\n/).filter(line => line.length > 0).map(v=>{
return path.join(codePath, v);
});
}
function replaceInFile(inputPath, outputPath, lines, title) {
// 这里要替换windows路径分隔符为正斜杠
// 因为在typ文件中,路径分隔符必须是正斜杠
// 例如:C:\Users\allen\company\pdf-code\src\main.rs
// 替换为:C:/Users/allen/company/pdf-code/src/main.rs
// 这样才能正确识别路径
const vals = lines.map(line => `"${line.replace(/\\/g, '/')}"`).join(',\n');
const data = fs.readFileSync(inputPath, 'utf-8');
const replaced = data.replace(new RegExp('{{title}}', 'g'), title)
.replace(new RegExp('{{filelist}}', 'g'), vals);
fs.writeFileSync(outputPath, replaced, 'utf-8');
}
function main() {
const [, , second, third] = process.argv;
if (!second || !third) {
console.error('Please provide both second and third arguments.');
return;
}
const temPath = 'tem.txt';
const outputPath = 'main.typ';
const filelist = 'filelist.txt';
const codePath = second;
const title = third;
console.log(`源码路径: ${second}`);
console.log(`标题: ${title}`);
const lines = readLinesToArray(filelist, codePath);
console.log(`源码文件数量: ${lines.length}`);
replaceInFile(temPath, outputPath, lines, title);
}
main()
typ模板文件
typ
#import "@preview/codly:1.3.0": *
#import "@preview/codly-languages:0.1.1": *
#set page(
paper: "a4",
margin: (
top: 1.5cm,
bottom: 1cm,
left: 1.5cm,
right: 1.5cm,
),
header: context [
#set align(right)
#set text(12pt)
{{title}}
#set text(8pt)
#h(1fr) 页码:
#counter(page).display(
"1 / 1",
both: true,
)
],
)
#set text(font: (
(name: "Noto Sans Mono", covers: "latin-in-cjk"),
"Noto Sans SC",
))
#show raw: set text(font: (
(name: "Noto Sans Mono", covers: "latin-in-cjk"),
"Noto Sans SC",
))
#show: codly-init.with()
#let files = ({{filelist}})
#let color = rgb("#646cff")
#for file_name in files {
codly(
zebra-fill: none,
display-icon: false,
header: raw(file_name),
header-repeat: true,
header-cell-args: (align: left),
header-transform: x => {
set text(fill: color)
strong(x)
line(length: 100%, stroke: 1pt + color)
},
languages: codly-languages,
lang-format: (_, _, _) => [],
number-align: right + top,
)
let lang = file_name.split(".").at(-1)
let text = read(file_name)
raw(text, block: true, lang: lang)
pagebreak()
}