Skip to content

官方微信公众号

橄榄田径 • openyunzhi-info

openyunzhi-info
办公技术分享

如何把软件源码批量打印成 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 的思路就变成这样:

  1. 准备一个 typ 模板文件,设置好页面、页边距、页眉、页码等。
  2. 将源码文件读取到 main.typ 文件中,设置好代码框的参数,字体、字号、标题。
  3. 使用 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 行代码,页眉标注该软件名称、版本号,右上角有页码。

pdf-code

实现的源码

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()
}
联系我们

关注 橄榄田径 公众号获取最新教程

qrcode

添加 15987804306 客服获取帮助

qrcode