从知网下了一篇论文,想在手机上翻两眼,微信里点开却是"文件已损坏"。换个阅读器,有的能开、有的白屏;发到电脑上又正常。折腾了半宿才弄明白:那个 .caj 根本不是什么特殊格式。
它本来就是 PDF,只是尾巴被塞了东西
知网给的 .caj,绝大多数用十六进制看第一行就是 %PDF-1.6——它就是一份标准 PDF,只是文件结尾被追加了一块知网自己的私有数据(记录下载者、订单号那类信息,XML 结构,FileProperty-Package)。
阅读器打开文件只看文件头,所以能认出来;但翻到尾部撞见一段不属于 PDF 规范的东西,有的就直接判"损坏"——微信内置预览和安卓自带阅读器是最容易翻脸的两个。
所以正确的动词不是"转换",是"清理重写":把尾部的垃圾块去掉,再把文件内部的索引重建一遍。
先认文件头,再决定走哪条路
.caj 是个套壳后缀,里面至少藏着三种完全不同的东西。别急着找工具,先看头四个字节:
head -c 4 "论文.caj"
| 文件头 | 里面是什么 | 怎么处理 |
|---|---|---|
%PDF-1.6 / %PDF-1.4 |
PDF 内核,尾部被污染(最常见) | mutool clean,一条命令 |
首字节 0xC8 |
私有文本指令流,正文不在页面里 | 专用脚本解析,见下文 |
CAJ / HN / KDH |
知网早期原生格式 | 开源工具 caj2pdf |
判断技巧还有一条:看结尾。用 tail -c 64 "论文.caj" 看一眼,如果最后不是 %%EOF,而是一段 </FileProperty-Package> 之类的 XML,那基本可以确诊是第一种。
最常见的那种:一条命令就完了
mutool clean -gggg -d -s "论文.caj" "论文.pdf"
mutool 是 mupdf 自带的工具,Termux 里 pkg install mupdf 就有了,几兆大小。四个参数各管一件事:-g 做垃圾回收(写四个是最高强度),-d 丢掉文档结构树和 XML 元数据残留,-s 重排并重建交叉引用表——最后这个是关键,前三步删掉的是对象,只有重建了索引,文件才算重新变成一个合法 PDF。
有两件事千万别做。
一是别直接把 .caj 改名成 .pdf。文件名骗得过电脑,骗不过阅读器——尾部那段垃圾还在,微信照样报损坏。二是别用文本编辑器或 sed 手工把尾部那段 XML 删掉。PDF 的交叉引用表里记着每个对象在文件中的字节偏移,硬删一段,后面所有偏移全部错位,文件从"部分打不开"直接变成"彻底报废"。清理必须交给会重建索引的工具。
转完必须验三件事
mutool info "论文.pdf" | grep -E "PDF-|Pages:"
mutool draw -F txt "论文.pdf" | head -25
head -c 20 "论文.pdf"; echo; tail -c 20 "论文.pdf"
页数和源文件一致;文字层能提出中文(能提=不是扫描件,可搜索可复制);首尾是 %PDF-1.6 和 %%EOF。
还有一步肉眼环节:把页面渲染成图看一眼,防乱码方块和错位。
D=$(mktemp -d)
mutool draw -o "$D/p%d.png" -r 100 "论文.pdf"
图片出来能直接看图核对:首页标题对不对、末页参考文献全不全、有没有缺字。这一步花的三十秒,比事后发现"少了一整页"再返工值钱。
那次的实际数字
源文件 198KB、六页;转完 527KB、还是六页。体积涨了一倍多,因为重建索引和对象表本身就是有成本的,但页面内容一个字节没动。文字层能正常提取全角中文,首页标题、作者、刊物年份和末页的责任编辑都在。

难对付的那种:正文就在文件里,工具却导不出来
第二种(首字节 0xC8)才是真的麻烦。这类文件用开源的 caj2pdf 跑,它会告诉你"成功",页数、插图数都报得出来——但导出的 PDF 里一张正文都没有,只剩几张插图。这不是工具写错了,是这类 CAJ 的正文既不是图片、也不是 PDF 页面,而是一段私有文本指令流,caj2pdf 根本没实现它。
好消息是文字完整地躺在文件里,只要按结构解出来,能做出带文字层、可搜索可复制的 PDF。实测的结构是这样:
- 页表从偏移
0x50开始,每页 20 字节,依次是文本段偏移、长度、图片数、页号、保留、下一段偏移; - 页文本段是"控制标签 + 字符单元"混在一起的小端 u16 流:
0x8001后面跟 Y 坐标,0x8002跟 X 坐标,0x80CC是一组字形记录,其余0x8000~0x80FF一律当标签按四字节跳过; - 剩下的四字节才对:前两个是横坐标,后两个是 GBK 码,而且是字节反序存放的——必须先按小端读出码值,再拼回
bytes([码>>8, 码&0xFF])用 gb18030 解码,直接按字节解全是乱码; - 字符不会和标签打架,因为 GBK 首字节从
0x81起,0x80本身非法,所以落在标签区间的一定是标签; - 英文字母数字另有一套映射:高字节
0xA0、低字节0xC1~0xFA时,真实字符就是低字节减0x80; - 插图记录 12 字节,后面紧跟图片数据,下一条记录要按"12 + 长度"往后跳;而且图片在文件里是上下倒序存的,嵌进 PDF 前必须翻转,否则整张图是倒的。
按这套解出来,五页正文按原坐标重新排版,插图按原页成组排在后面,一共七页,文字层可搜索。
后来又修的三处
能用之后复查,发现三处值得改,改的都不是解码逻辑:
一是双栏论文的文字层原来左右交替,复制出来是碎片。改成按基线聚类、按行内大空隙切栏段,先输出左栏读完再输出右栏——字符还画在原坐标,版面不变,但复制和搜索都连贯了。
二是年份、页码、分类号这类全角数字(2007、G623),搜 2007 搜不到。改成画字前把全角数字字母转半角,全角标点一律不动,否则中文逗号顿号会被破坏。一份文件修正了四百多个字。
三是四张插图原来各占一页,白页太多。改成按原页成组、一页两张,带"原文第 N 页 插图"的标题,九页压到七页。
还有两件是格式本身的限制,改不了:插图没法回到正文原位(这类 CAJ 里图片不带页坐标,只记了属于哪一页),只能成组排在正文后面;宽字距的英文页眉在文字层里可能被插进多余空格,复制下来要手工去一下。
我踩过的几个坑
/tmp 在 Termux 里不可写,报 Permission denied,中间图一律用 mktemp -d 生成目录。
mutool clean -s 拿 /dev/null 当输出"验证"会出事——它会反推出一个 out.pdf 悄悄丢在当前目录,你以为没留文件,其实留了。
中文名、空格、引号三样凑齐的时候,变量不加双引号必报"文件不存在",因为 shell 按空格把文件名拆成了两个参数。
输出名和源文件名只差扩展名,手滑写成同一个路径就把原件覆盖了。原始 .caj 留着,出问题能重来。
一整个压缩包全是 caj 的时候
现实里更常见的是"导师发来一个压缩包,里面十几篇"。流程是:解压出所有 .caj → 逐个转换(脚本按文件头自动分流)→ 每份都核一遍页数、别跳 → 重新打包。
Termux 里没有 zip 命令(只有 unzip),要用 Python 的 zipfile 模块打包,中文名才不乱码:
python3 -c "
import zipfile,glob,os
with zipfile.ZipFile('论文_PDF版.zip','w',zipfile.ZIP_DEFLATED) as z:
for f in sorted(glob.glob('*.pdf')): z.write(f, os.path.basename(f))"
耗时大头在原生型:一百多页的 caj2pdf 单份要跑几分钟,属正常。其余两种都是秒级。

转不动就别硬转
原生型(CAJ/HN/KDH)真转不动的时候,有三条退路,按省事程度排:
一、回知网重新下载,下载页选"PDF 下载"而不是 CAJ。这是最省事的一条,值得优先考虑——很多折腾都是在为一次点错买单。
二、手机上装 CAJViewer 官方 App,打开后走"另存为"或"打印为 PDF"。
三、电脑端用知网研学(E-Study)打开,导出 PDF。
工具能做到哪一步是有边界的,识别出边界、换条路,比在一个导图像的工具上磨一个小时划算。