找回密码
 快速注册
搜索
查看: 70|回复: 4

PDF的CMAP table(字符映射表)

[复制链接]

3149

主题

8386

回帖

6万

积分

$\style{scale:11;fill:#eff}꩜$

积分
65391
QQ

显示全部楼层

hbghlyj 发表于 2023-6-28 21:43 |阅读模式
为了测试,用lualatex生成单页PDF: $type document_4CBF_8811.pdf (2.98 KB, 下载次数: 0)
字体为FandolSong,只有一个字“龚”(texlive.net
查看PDF的第1页的内容:
  1. import fitz
  2. doc = fitz.open("document_4CBF_8811.pdf")
  3. page = doc[0]
  4. for xref in page.get_contents():
  5.     stream = doc.xref_stream(xref).decode()
  6.     print(stream)
复制代码

输出:
  1. BT
  2. /F23 9.96264 Tf
  3. 1 0 0 1 148.712 707.125 Tm [<0701>]TJ
  4. 1 0 0 1 303.133 139.255 Tm [<0012>]TJ
  5. ET
复制代码
这段 PDF 代码是渲染文本的指令。以下是每行的解释:
BT:开始(begin)文本对象,表明接下来的指令将指定文本渲染操作。
Tf:设置字体(font)和字号。/F23引用标识符为“F23”的字体资源,9.96264 指定字体大小。
Tm:设置矩阵(matrix)。此处$\pmatrix{1&0\\0&1\\148.712&707.125}$是平移到(148.712,707.125)
TJ:显示文本。在本例中,它包含单个字符0701。尖括号<...>指示该字符串采用十六进制编码。
Tm:与前面类似,设置一个新的矩阵,为下一个文本渲染操作指定新的平移位置。
TJ:与前面类似,显示字符串0012,十六进制编码。
ET:结束(end)文本对象,表明文本渲染指令已经完成。
总之,这段代码设置字体和字号,设置平移,显示文本“龚”(编码0701),再设置新的平移,(字体和字号不变)显示文本“1”(编码0012)。

问:1是哪来的?明明只打了一个字?
答:1是页码。若不想要页码,在TeX中可用\thispagestyle{empty}去除。

3149

主题

8386

回帖

6万

积分

$\style{scale:11;fill:#eff}꩜$

积分
65391
QQ

显示全部楼层

 楼主| hbghlyj 发表于 2023-6-28 22:00
本帖最后由 hbghlyj 于 2023-6-28 23:57 编辑 问:为什么“龚”的编码是0701?为什么“1”的编码是0012?
答:用PyMupdf查看FandolSong的CMAP table:
  1. import fitz
  2. doc = fitz.open("document_4CBF_8811.pdf")
  3. page = doc[0]
  4. font='FandolSong'
  5. def get_key(xref,key):
  6.     return int(doc.xref_get_key(xref,key)[1].split()[0].replace("[", ""))
  7. for font_tuple in page.get_fonts():
  8.     if font_tuple[3].split("+", maxsplit=1)[1].startswith(font):
  9.         for line in doc.xref_stream(get_key(font_tuple[0],'ToUnicode')).decode().splitlines():
  10.             print(line)
复制代码
输出
%!PS-Adobe-3.0 Resource-CMap
%%DocumentNeededResources: ProcSet (CIDInit)
%%IncludeResource: ProcSet (CIDInit)
%%BeginResource: CMap (TeX-NSHQXR-FandolSong-Regular-0)
%%Title: (TeX-NSHQXR-FandolSong-Regular-0 TeX NSHQXR-FandolSong-Regular 0)
%%Version: 1.000
%%EndComments
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo
<< /Registry (TeX)
/Ordering (NSHQXR-FandolSong-Regular)
/Supplement 0
>> def
/CMapName /TeX-Identity-NSHQXR-FandolSong-Regular def
/CMapType 2 def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
0 beginbfrange
endbfrange
2 beginbfchar
<0012> <0031>
<0701> <9F9A>

endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
%%EndResource
%%EOF

其中beginbfchar...endbfchar是CID到Unicode编码的映射
右图可见这两个Unicode编码对应的字
Screenshot 2023-06-28 150913.png

参见:
Editing text in PDF
Editing CMap / ToUnicode to achieve correct character mapping when extracting text
How are Embedded CMAP tables defined in a PDF File?
hand-modify-pdf.md
Inside the PDF File Format
PDF reference
Developing with PDF by Leonard Rosenthol

3149

主题

8386

回帖

6万

积分

$\style{scale:11;fill:#eff}꩜$

积分
65391
QQ

显示全部楼层

 楼主| hbghlyj 发表于 2023-6-29 05:56
这个PDF的字母A-Za-z的字体是LMRoman10
将2#脚本第4行改为font='LMRoman10'同样地提取出CMAP table
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CMapName /BYPSOO+LMRoman10-Regular-UTF16 def
/CMapType 2 def
/CIDSystemInfo <<
  /Registry (Adobe)
  /Ordering (UCS)
  /Supplement 0
>> def
1 begincodespacerange
<0000> <FFFF>
endcodespacerange
59 beginbfchar
<001B> <0041>
<001C> <0061>
<0022> <0042>
<0023> <0062>
<0028> <005B>
<0029> <005D>
<002B> <0063>
<002C> <003A>
<002E> <0044>
<002F> <0064>
<0032> <0065>
<0033> <0038>
<0036> <0046>
<0037> <0066>
<0038> <0035>
<0039> <0034>
<003A> <0047>
<003B> <0067>
<003E> <0048>
<003F> <0068>
<0040> <002D>
<0041> <0049>
<0042> <0069>
<0046> <006B>
<0074> <0078>
<0076> <0079>
<0077> <005A>
<0078> <007A>
<0079> <0030>
<00C7> <2022>
<0189> <00AF>

endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end

下面的脚本可以查询CID:
  1. def repl(search):
  2.     map={'\u0041':'001B',
  3.         '\u0061':'001C',
  4.         '\u0042':'0022',
  5.         '\u0062':'0023',
  6.         '\u005B':'0028',
  7.         '\u005D':'0029',
  8.         '\u0063':'002B',
  9.         '\u003A':'002C',
  10.         '\u0044':'002E',
  11.         '\u0064':'002F',
  12.         '\u0065':'0032',
  13.         '\u0038':'0033',
  14.         '\u0046':'0036',
  15.         '\u0066':'0037',
  16.         '\u0035':'0038',
  17.         '\u0034':'0039',
  18.         '\u0047':'003A',
  19.         '\u0067':'003B',
  20.         '\u0048':'003E',
  21.         '\u0068':'003F',
  22.         '\u002D':'0040',
  23.         '\u0049':'0041',
  24.         '\u0069':'0042',
  25.         '\u006B':'0046',
  26.         '\u0078':'0074',
  27.         '\u0079':'0076',
  28.         '\u005A':'0077',
  29.         '\u007A':'0078',
  30.         '\u0030':'0079',
  31.         '\u2022':'00C7',
  32.         '\u00AF':'0189'}
  33.     return ''.join([map.get(x,x) for x in search])
  34. print(repl('hy'))
复制代码
输出
003F0076

3149

主题

8386

回帖

6万

积分

$\style{scale:11;fill:#eff}꩜$

积分
65391
QQ

显示全部楼层

 楼主| hbghlyj 发表于 2023-6-29 06:56
3#是查询单个字体的CMAP table
下面的脚本将第7页的所有字体的CMAP table合并成map,并查询CID:
  1. import fitz
  2. doc = fitz.open("waiweifen.pdf")
  3. page = doc[7]
  4. map = {}
  5. def get_key(xref,key):
  6.     return int(doc.xref_get_key(xref,key)[1].split()[0].replace("[", ""))
  7. for font_tuple in page.get_fonts():
  8.     try:
  9.         found = False
  10.         for line in doc.xref_stream(get_key(font_tuple[0],'ToUnicode')).decode().splitlines():
  11.             if 'beginbfchar' in line:
  12.                 found = True
  13.                 continue
  14.             if 'endbfchar' in line:
  15.                 found = False
  16.                 continue
  17.             if found:
  18.                 cid = line.split()[0][1:-1]
  19.                 unicode = chr(int("0x" +line.split()[1][1:-1], 16))
  20.                 map[unicode] = cid
  21.     except:
  22.         continue
  23. def replace(search):
  24.     return ''.join([map.get(x,x) for x in search])
  25. print(replace('hy'))
复制代码
输出
003F0076

3149

主题

8386

回帖

6万

积分

$\style{scale:11;fill:#eff}꩜$

积分
65391
QQ

显示全部楼层

 楼主| hbghlyj 发表于 2023-7-1 01:18
反过来可以由CID查询Unicode,在PDF十六进制字符串的解码要用到:
  1. import fitz
  2. doc = fitz.open("waiweifen.pdf")
  3. page = doc[7]
  4. map = {}
  5. def get_key(xref,key):
  6.     return int(doc.xref_get_key(xref,key)[1].split()[0].replace("[", ""))
  7. for font_tuple in page.get_fonts():
  8.     try:
  9.         found = False
  10.         for line in doc.xref_stream(get_key(font_tuple[0],'ToUnicode')).decode().splitlines():
  11.             if 'beginbfchar' in line:
  12.                 found = True
  13.                 continue
  14.             if 'endbfchar' in line:
  15.                 found = False
  16.                 continue
  17.             if found:
  18.                 cid = line.split()[0][1:-1]
  19.                 unicode = chr(int("0x" +line.split()[1][1:-1], 16))
  20.                 map[cid] = unicode
  21.     except:
  22.         continue
  23. def chunks(lst, n):
  24.     """Yield successive n-sized chunks from lst."""
  25.     for i in range(0, len(lst), n):
  26.         yield lst[i:i + n]
  27. def replace(search):
  28.     return ''.join([map.get(x,x) for x in chunks(search.upper(),4)])
  29. print(replace('0048007400620054003f0076'))
复制代码

输出
lxsphy

手机版|悠闲数学娱乐论坛(第3版)

GMT+8, 2025-3-4 15:53

Powered by Discuz!

× 快速回复 返回顶部 返回列表