# 答题卡制作器 · Answer Sheet Builder

<p align="center">
  <b>像搭积木一样，制作考试答题卡。</b><br>
  <i>Build exam answer sheets, block by block.</i>
</p>

<p align="center">
  <a href="https://github.com/DC1024/answer-sheet-builder/releases/tag/v1.4.1"><img alt="version" src="https://img.shields.io/badge/version-1.4.1-blue"></a>
  <a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-green"></a>
  <img alt="no backend" src="https://img.shields.io/badge/backend-none-success">
  <a href="https://github.com/DC1024/answer-sheet-builder/actions/workflows/docker.yml"><img alt="docker build" src="https://github.com/DC1024/answer-sheet-builder/actions/workflows/docker.yml/badge.svg"></a>
  <a href="https://github.com/DC1024/answer-sheet-builder/stargazers"><img alt="stars" src="https://img.shields.io/github/stars/DC1024/answer-sheet-builder?style=social"></a>
</p>

<p align="center">
  简体中文 &nbsp;·&nbsp; <a href="README_EN.md">English</a> &nbsp;·&nbsp; <a href="CHANGELOG.md">更新日志</a> &nbsp;·&nbsp; <a href="https://dc1024.github.io/answer-sheet-builder/app.html">在线试用</a>
</p>

---

## 这是什么？

**答题卡制作器**是一个**纯前端**的可视化工具，用来快速制作考试答题卡并打印 / 导出 PDF。
无需后端、无需注册、数据不出本机；所有逻辑都在浏览器里完成，打包成 Docker 镜像即可在内网部署。

> 在线试用：打开 `app.html`（或在 Docker 部署后访问站点根路径）即可开始制作。

## 功能特性

| 能力 | 说明 |
| --- | --- |
| 🧩 **模块化题型** | 选择题 / 填空题 / 解答题 / 考生信息栏 / 自定义编辑区，均为独立模块，可添加、复制、删除、**拖拽排序**。 |
| 📄 **固定纸张分页** | 纸张尺寸**固定不随内容变化**；内容按「第一面 → 第二面 → 第二页第一面……」顺序填充。A3 每面双栏（左右各一张 A4 规格）。 |
| 🔤 **填涂 / 手写** | 选择题支持**中括号 `[A]` 填涂**与**横线手写**；选项数量任意（**超过 26 个用 `AA`/`AB`… 续排**）。列数在**渲染时实测**单题真实宽度后决定，**每题完整排在一行内**、排不下的整题换行，绝不溢出。**行间距（mm）**可调，且**标题到第一行、行与行的间距严格一致**。 |
| 🎯 **四角定位点** | 支持**方块 / 三角**两种样式，边长可调（2–10mm）。每一面只要**有题目内容**就自动补四角定位点，空白面不加；页边距会按定位点尺寸自动预留，**绝不覆盖任何内容**；打印时强制输出底色，供阅卷机定位。 |
| ✎ **多级填空** | 支持 `11（1）`、`11（2）①/②` 这类多级小题，编号**按层级自动区分**（小题 `（1）（2）` → 小小题 `①②` → 更深 `a) b)`）；**一行没用完就继续放下一题，排满才换行**；每个空**长度**与**行间距**可调，**大题之间与折行后的行距严格一致**。 |
| 📐 **解答题横线 / 插图** | 作答区可选「空白」或「横线」样式，**横线间距**与**作答区高度**可调 —— 高度**也可以直接在预览区拖动作答区下边缘实时调整**（40–400mm，松手即记一个撤销点）；**每题可插一张图片**：选中模块后把鼠标停在预览区某作答区上按 **Ctrl+V** 即贴进该题，图片**叠加在作答区内**（不占高度、不挤压横线），**九宫格定位** + 宽度可调，超出部分自动裁切（本地压缩，仅存浏览器）。 |
| 📝 **主观题人工阅卷** | 选择题 / 填空题 / 解答题都能勾「**人工阅卷**」：填**满分**，可选再拆**小问**（各自给分）。导出模板时带上**整题一块的作答区坐标**；扫描端据此裁出作答区图，阅卷工作台里对着作答区给分，成绩单自动多出「**客观分 / 主观分 / 总分**」三列。两种给分模式（整题给一个分 / 按小问逐项给）由模板决定，分值**夹到各自满分**。**纯选择题卷子的行为一个字没变。** |
| 🪪 **考生信息栏** | 可增删「班级 / 姓名 / 考号」等手写栏；**考号填涂区**为中括号方块、**框内直接显示数字**，位数上限 20。填涂区宽度默认**不超过页面的 1/4**（可调 10–60%），上限内把方框撑到 **3–5mm**（可填涂、可读），余量交给右侧手写栏拉满；只有连 3mm 都放不下时才自动放宽，**永不超出纸张**。 |
| ⌨️ **快捷键** | **Ctrl+C / Ctrl+X / Ctrl+V** 复制、剪切、粘贴模块（选中「图片」模块时可直接粘贴剪贴板图片）；**Ctrl+Z / Ctrl+Y** 撤销、重做（快照式，最多 100 步）。 |
| 🖼 **图片插图** | 可插入图片并调整**宽度（%）与对齐（左/中/右）**；上传时在本机自动压缩，**仅存浏览器缓存、不上传服务器**，打印 / 导出 PDF 时正常显示。 |
| ⬛ **统一黑色边框** | 每个题型区块都带黑色边框，结构清晰、打印友好。 |
| 🎛 **通用样式** | 每个模块可单独设置**字号**与**对齐（左/中/右）**。 |
| 💾 **模板复用** | 本地保存（localStorage）、载入；支持导出 / 导入 **JSON** 模板。 |
| 🖨 **打印 / 导出 PDF** | 浏览器直接打印或另存 PDF；支持**双面长边翻转**。 |

## 界面预览

```
┌───────────────┬─────────────────────────────┬───────────────┐
│ ① 添加题型     │          预览（固定分页）      │ ③ 属性设置     │
│ 选择题/填空/…  │   ┌───────────────────┐     │ 题数 / 样式 /  │
├───────────────┤   │    A3 第一面（双栏） │     │ 空格 / 字号 /  │
│ ② 答题卡结构   │   ├───────────────────┤     │ 对齐 / 间距 …  │
│ （拖拽排序）   │   │    A3 第二面        │     │               │
└───────────────┴─────────────────────────────┴───────────────┘
```

## 快速开始

### 方式零：Windows 免安装版（学校机房 / 不想装环境，推荐）

到 [Releases](https://github.com/DC1024/answer-sheet-builder/releases) 下载两个 zip，解压、双击 exe 即可，
**不需要装 Python、不需要 Docker、不需要联网**：

| 下载 | 双击这个 | 说明 |
| --- | --- | --- |
| `...-cardmaker-windows-x64.zip`（约 9 MB） | `答题卡制作器.exe` | 制卡端。会自动起一个只监听 `127.0.0.1` 的本地服务并打开浏览器（页面是 ES Module，`file://` 下打不开） |
| `...-scanner-windows-x64.zip`（约 20 MB） | `答题卡扫描服务.exe` | 扫描端。默认 <http://127.0.0.1:8081>，**首次打开会让你创建管理员账号** |

> 扫描端这个包已经瘦身：手写 A-D 的 CNN 改走 **ONNX Runtime** 推理（不再带 290 MB 的 CPU 版
> PyTorch，`torch_cpu.dll` 没了），zip 从 ~210 MB 降到 **~20 MB**。真实手写净准确率 75.5% → 99.2%，
> 精度零退化（ONNX 与 torch 在 249 个真实字形上逐一比对、最大偏差 < 1e-5）。只做填涂识别、不需要
> 手写的话，加 `--no-cnn` 启动能省一点内存（识别自动退回纯 OpenCV）。

```bat
答题卡制作器.exe --port 8899          :: 指定端口（默认自动挑一个空闲端口）
答题卡扫描服务.exe --port 8081 --data D:\asb   :: 数据和校对图放哪儿
答题卡扫描服务.exe --no-cnn           :: 关掉手写 CNN，省内存（纯 OpenCV 照常跑）
```

默认数据目录 `%LOCALAPPDATA%\asb-scanner\data`（拷走整个文件夹就是完整备份）。
每个 zip 里都有一份 `使用说明.txt`。杀毒软件/首次运行若拦截，放行即可 —— PyInstaller 单文件包被误报是常态。

### 方式一：Docker（推荐给服务器部署）

```bash
# 1) 构建镜像
docker build -t answer-sheet-builder .

# 2) 运行
docker run -d --name asb -p 8080:80 answer-sheet-builder

# 3) 浏览器打开
#    http://<服务器IP>:8080
```

或使用 compose：

```bash
docker compose up -d
```

### 方式二：本地 / 任意静态服务器

项目使用 **ES Module**，必须通过 HTTP 访问（不能直接双击打开 HTML）：

```bash
cd answer-sheet-builder
python3 -m http.server 8080
# 浏览器打开 http://localhost:8080/app.html
```

### 方式三：拉取预构建镜像（GHCR）

CI 会在每次推送 `main` 或打 `v*` 标签时**自动构建并推送镜像**到 GitHub Container Registry：

```bash
docker pull ghcr.io/dc1024/answer-sheet-builder:latest
docker run -d --name asb -p 8080:80 ghcr.io/dc1024/answer-sheet-builder:latest
# 打开 http://<服务器IP>:8080
```

> 镜像名全小写：`ghcr.io/dc1024/answer-sheet-builder`。标签包含 `latest`、分支名、`v1.2.3` / `1.2`、以及 `sha-xxxxxxx`。
> 若拉取提示无权限，请在仓库 **Packages** 里把该包可见性设为 Public（首次推送后生效）。

## 使用步骤

1. 在左侧「① 添加题型」点击需要的题型，会自动加入结构列表。
2. 在「② 答题卡结构」拖动 `⠿` 手柄排序，或点 ↑ / ↓ 调整；点 ⧉ 复制、✕ 删除。
3. 选中模块，在右侧「③ 属性设置」配置题数、样式、空格、高度、字号、对齐等。
4. 顶栏选择纸张（A3 / A4）与方向（竖版 / 横版），中间预览按**固定尺寸逐面**显示。
5. 点「🖨 打印 / 导出 PDF」，在打印对话框选择对应纸张、勾选「双面（长边翻转）」，另存为 PDF 或直接打印。

## 配套：扫描识别服务（自动阅卷）

排好卷子只是上半场 —— 学生作答、收卷之后，`🎯 阅卷模板` 能把这份卷子变成机器可读的坐标文件，
交给本仓库自带的 **[scanner/](scanner/)** 服务自动识读选择题并出班级统计。

```
制卡端排卷  ──▶  点「🎯 阅卷模板」  ──▶  asb-omr-template-*.json  ──▶  scanner 服务
                                                                          │
                                     扫描 / 拍照 ────────────────────────┘
                                                                          │
                                                          每题答案 + 存疑标注 + 班级统计 + CSV
```

导出的模板里含**每个填涂圈的毫米坐标**、四角定位点、纸张尺寸与题号选项；考生信息栏开启了
**考号填涂区**时，连同考号每一位的格子坐标一起导出（模板格式 `asb-omr/2`）。扫描服务据此做
透视矫正后逐圈采样，纯 OpenCV 实现、不下载模型、可离线运行。实测（6 份合成卷 × 20 题）：

| 场景 | 答案 | 卷面考号 |
| --- | --- | --- |
| 干净扫描（300dpi 级） | **100%** | **100%** |
| 手机拍照模拟（透视 + 明暗 + 模糊 + 噪声 + JPEG） | **100%** | **100%** |

未涂题自动标 `blank`、浅涂题标 `faint`、一题涂两个标 `multi`。

**批量模式**：一次丢一个班 —— 拖入 **zip / 整个文件夹 / 一堆散图**，自动归组、多页自动合并；
再传一份 `考号,姓名,班级` 的名单，姓名班级自动贴上，考号对不上、页序异常、名单里没交卷的
都会列进**待人工确认队列**。成绩 CSV 带考号/姓名/班级列。

**卷子归谁名下，优先看卷面**：学生涂的考号就在答题卡上，识别出来直接用来归组 ——
**不需要给每个学生发二维码，也不需要把 50 个文件改名成考号**。文件名里写了考号（`2026010234_1.png`
或每个学生一个目录）会用来互相印证；两者不一致时**不会擅自改动**，而是把两边都报出来让人确认。
名单还能把只差一位的考号认出来（疑似涂错一位）、把卷面漏涂的那一位补全。

> 前提：卷子里要有**填涂圈模式**的选择题，或开启**考号填涂区**（两者至少其一）；
> 全是手写作答模式时会提示「没有可识别的填涂圈」。

**主观题（解答题 / 填空题）人工阅卷**：这套工具不只判选择题。在制卡端给任意一道题勾上
「**人工阅卷**」、填好**满分**（可再拆**小问**，每个小问各自给分），导出模板时这道题就会带上
**整题一块的作答区坐标**。扫描识别时按这个坐标从**校正后的干净图**上裁出作答区，阅卷工作台里
右边默认显示的就是**这道题的作答区**（够大、看得清字），一键可切回**整页校对**；每道主观题一张
打分卡片，填完分数弹窗底部立刻重算 `客观 19 ＋ 主观 13 ＝ 32 / 35`。

给分方式由模板决定：**设了小问**就按小问逐项给分再求和，**没设**就是整题给一个分；每一项都会
**夹到自己的满分**，手滑把 8 打成 80 不会把总分炸掉。有主观题的考试，成绩单和导出的 CSV 自动从
两列变成「**客观分 / 主观分 / 总分**」三列（满分写在 `X-Score-Max` 头里）；**纯选择题的考试仍然
是原来的两列**，老师的手感和已有脚本都不受影响。

**设置与检查更新**：制卡端工具栏的 `⚙ 设置`、扫描端「设置 → ① 设置」卡片里，可以**开关自动检查更新**、
**手动点一下「检查更新」**、看当前版本号。学校内网连不上 GitHub 是常态，所以这种时候只会显示
「暂时查不到」并说明原因 —— **不算错误、不弹红**。换源 / 内网镜像 / fork 出去自己发版，
用 `ASB_UPDATE_API` 指一下即可。

## 目录结构（模块化）

```
answer-sheet-builder/
├── index.html              # 宣传页（GitHub Pages 首页）
├── app.html                # 制作器入口
├── Dockerfile              # nginx 托管静态资源
├── docker-compose.yml      # 一键部署
├── assets/
│   ├── css/style.css       # 全部样式（含打印 @media print）
│   └── js/
│       ├── app.js          # 主程序：面板 / 结构 / 属性 / 预览 / 打印装配
│       └── core/
│           ├── util.js     # esc / uid / 深拷贝 / 通用样式
│           ├── store.js    # 全局状态（blocks 数组、增删改移、持久化）
│           ├── registry.js # 题型注册表（聚合模块，便于扩展）
│           ├── preview.js  # 固定纸张分页引擎
│           ├── uistate.js  # 纯运行期 UI 状态（当前作答区等，不写入模板）
│           └── omr.js      # 导出阅卷模板（每面定位点 + 每个填涂圈的 mm 坐标）
│       └── blocks/         # 各题型模块（每个含 defaults / configUI / render）
│           ├── info.js
│           ├── singleChoice.js
│           ├── fillBlank.js
│           ├── answer.js
│           ├── image.js
│           └── custom.js
├── scanner/                # 配套：扫描识别服务（Python + OpenCV + Flask，见 scanner/README.md）
├── LICENSE
├── CHANGELOG.md
└── README.md / README_EN.md
```

**扩展新题型**：在 `assets/js/blocks/` 新建一个模块（导出 `{ type, name, icon, defaults, configUI, render }`），再在 `core/registry.js` 的数组里 import 并加入即可，无需改动其他代码。

## 打印提示

- 使用 Chrome / Edge 打印效果最佳；页边距请选「**无 / None**」，每页内部留白由页面卡片自身控制。
- 预览区**每张卡片 = 一个「面」**。A3 双面打印时勾选「双面打印 / 长边翻转」，即可按「第一面 → 第二面 → 第二页第一面……」顺序输出。
- 每个题型区块都与预览一致地带上黑色边框。

## 技术栈

- 原生 **ES Module**（无框架、无构建步骤）
- **CSS** 多栏 + `repeating-linear-gradient`（横线）+ `@media print`
- 原生 **Docker + nginx:alpine** 托管

## 许可证

[MIT](LICENSE) © 2026 DC · 完全免费开源，可自由使用、修改与分发。
