PingFang 字体的提取与使用

2019-02-13 2956点热度 3人点赞

在 windows 上想用 PingFang 字体看看效果怎么办?直接拖过来却不好用?你需要的是重新提取。


1 、 PingFang 字体相关

苹方(英语:PingFang)是苹果公司委托威锋数位为 iOS 9 及 OS X El Capitan 所开发的专有字体家族,属于无衬线黑体。此字体最早作为 iOS 9 的默认中文字体于 2015 年 6 月 8 日在旧金山的 WWDC 2015 上公布,支持简体中文、繁体中文、日文和韩文,其中繁体中文包括香港字形和台湾字形两个版本,且提供了七种字重,取代了从 iOS 4 以来一脉相传的华文系黑体。

https://zh.wikipedia.org/wiki/%E8%8B%B9%E6%96%B9

请注意:PingFang 字体版权为 Apple 公司,非授权情况下使用即为违规


2 、前提准备

本文优先使用 Linux 系统,Python2.7 版本


3 、提取 PingFang 字体

3.1 、找到 PingFang 字体

在 MacOS 系统中,你需要到下面这个目录中找到 PingFang(需要 OS X 10.11 El Capitan 或更高版本的系統)

/System/Library/Fonts/

找到后将文件通过任意方法复制到 Linux 系统上。


3.2 、解构 PingFang 字体文件

首先你需要下载一个别人制作的 Python 脚本 [ 链接 ]

脚本详细内容如下

# otc2otf.py v1.0 Jan 14 2014

__help__ = """
AFDKOPython otc2otf.py  [-w] <font.ttc>

example:
 AFDKOPython otc2otf.py  -w  temp.ttc

Report tags and offsets. Optionally extract all OpenType fonts from the parent OpenType Collection font.

-w  Optional. Extract and write all sfnt fonts as separate font files.
"""


import sys
import os
import struct

class OTCError(TypeError):
	pass

class FontEntry:

	def __init__(self, sfntType, searchRange, entrySelector, rangeShift):
		self.sfntType = sfntType
		self.searchRange = searchRange
		self.entrySelector = entrySelector
		self.rangeShift = rangeShift
		self.tableList = []
		self.fileName = "temp.tmp.otf"
		self.psName = "PSNameUndefined"
		
	def append(self, tableEntry):
		self.tableList.append(tableEntry)

class TableEntry:
	
	def __init__(self, tag, checkSum, length):
		self.tag = tag
		self.checksum = checkSum
		self.length = length
		self.data = None
		self.offset = None
		self.isPreferred = False

ttcHeaderFormat = ">4sLL"
ttcHeaderFormatDoc = """
		> # big endian
		TTCTag:                  4s # "ttcf"
		Version:                 L  # 0x00010000 or 0x00020000
		numFonts:                L  # number of fonts
		# OffsetTable[numFonts]: L  # array with offsets from beginning of file
		# ulDsigTag:             L  # version 2.0 only
		# ulDsigLength:          L  # version 2.0 only
		# ulDsigOffset:          L  # version 2.0 only
"""


ttcHeaderSize = struct.calcsize(ttcHeaderFormat)

offsetFormat = ">L"
offsetSize = struct.calcsize(">L")

sfntDirectoryFormat = ">4sHHHH"
sfntDirectoryFormatDoc = """
		> # big endian
		sfntVersion:    4s
		numTables:      H    # number of tables
		searchRange:    H    # (max2 <= numTables)*16
		entrySelector:  H    # log2(max2 <= numTables)
		rangeShift:     H    # numTables*16-searchRange
"""

sfntDirectorySize = struct.calcsize(sfntDirectoryFormat)

sfntDirectoryEntryFormat = ">4sLLL"
sfntDirectoryEntryFormatDoc = """
		> # big endian
		tag:            4s
		checkSum:       L
		offset:         L
		length:         L
"""

sfntDirectoryEntrySize = struct.calcsize(sfntDirectoryEntryFormat)


nameRecordFormat = ">HHHHHH"
nameRecordFormatDoc = """
		>	# big endian
		platformID:	H
		platEncID:	H
		langID:		H
		nameID:		H
		length:		H
		offset:		H
"""
nameRecordSize = struct.calcsize(nameRecordFormat)

		
def parseArgs(args):
	writeOTF = False
	fontPath = None
	argn = len(args)
	i = 0
	while i < argn:
		arg  = args[i]
		i += 1
		
		if arg[0] != '-':
			fontPath = arg
		elif arg == "-w":
			writeOTF = True
		elif (arg == "-u") or (arg == "-h"):
			print __help__
			raise OTCError()
		else:
			raise OTCError("Unknown option '%s'." % (arg))
			
	if fontPath == None:
		raise OTCError("You must specify an OpenType Collection font as the input file..")
	
	allOK = True
	if not os.path.exists(fontPath):
		raise OTCError("Cannot find '%s'." % (fontPath))
		
	fp = file(fontPath, "rb")
	data = fp.read(4)
	fp.close()
	
	if data != 'ttcf':
		raise OTCErrr("File is not an OpenType Collection file: '%s'." % (fontPath))

	return fontPath, writeOTF

def getPSName(data):
	format, n, stringOffset = struct.unpack(">HHH", data[:6])
	expectedStringOffset = 6 + n * nameRecordSize
	if stringOffset != expectedStringOffset:
		# XXX we need a warn function
		print "Warning: 'name' table stringOffset incorrect.",
		print "Expected: %s; Actual: %s" % (expectedStringOffset, stringOffset)
	stringData = data[stringOffset:]
	data = data[6:]
	psName = "PSNameUndefined"
	for i in range(n):
		if len(data) < 12:
			# compensate for buggy font
			break
		platformID, platEncID, langID, nameID, length, offset = struct.unpack(nameRecordFormat, data[:nameRecordSize])
		data = data[nameRecordSize:]
		if not ((platformID, platEncID, langID, nameID) == (1, 0, 0, 6)):
			continue
			
		psName = stringData[offset:offset+length]
		assert len(psName) == length

	return psName

def readFontFile(fontOffset, data):
	sfntType, numTables, searchRange, entrySelector, rangeShift = struct.unpack(sfntDirectoryFormat, data[fontOffset:fontOffset + sfntDirectorySize])
	fontEntry = FontEntry(sfntType, searchRange, entrySelector, rangeShift)
	entryData = data[fontOffset + sfntDirectorySize:]
	i = 0
	seenGlyf = False
	while i < numTables:
		tag, checksum, offset, length = struct.unpack(sfntDirectoryEntryFormat, entryData[:sfntDirectoryEntrySize])
		tableEntry = TableEntry(tag, checksum, length)
		tableEntry.offset = offset
		tableEntry.data = data[offset:offset+length]
		fontEntry.tableList.append(tableEntry)
		if tag == "name":
			fontEntry.psName = getPSName(tableEntry.data)
		elif tag == "glyf":
			seenGlyf = True
		entryData =  entryData[sfntDirectoryEntrySize:]
		i += 1
	if seenGlyf:
			fontEntry.fileName = fontEntry.psName + ".ttf"
	else:
			fontEntry.fileName = fontEntry.psName + ".otf"
		
	print "%s" % (fontEntry.psName)
	for tableEntry in fontEntry.tableList:
		print "\t%s checksum: 0x%08X, offset: 0x%08X, length: 0x%08X" % (tableEntry.tag, tableEntry.checksum, tableEntry.offset, tableEntry.length)
	return fontEntry


def writeOTFFont(fontEntry):

	dataList = []
	numTables = len(fontEntry.tableList)
	# Build the SFNT header
	data = struct.pack(sfntDirectoryFormat, fontEntry.sfntType, numTables, fontEntry.searchRange, fontEntry.entrySelector, fontEntry.rangeShift)
	dataList.append(data)
	
	fontOffset = sfntDirectorySize + numTables*sfntDirectoryEntrySize
	print fontOffset
	# Set the offsets in the tables.
	for tableEntry in fontEntry.tableList:
		tableEntry.offset = fontOffset
		fontOffset += tableEntry.length
	
	# build table entries in sfnt directory
	for tableEntry in fontEntry.tableList:
		tableData = struct.pack(sfntDirectoryEntryFormat, tableEntry.tag, tableEntry.checksum, tableEntry.offset, tableEntry.length)
		dataList.append(tableData)
	
	# add table data to font.
	for tableEntry in fontEntry.tableList:
		tableData = tableEntry.data
		dataList.append(tableData)
	
	fontData = "".join(dataList)
	
	fp = file(fontEntry.fileName, "wb")
	fp.write(fontData)
	fp.close()
	return

def run(args):
	fontPath, writeOTF = parseArgs(args)
	print "Input font:", fontPath
	

	fp = file(fontPath, "rb")
	data = fp.read()
	fp.close()

	TTCTag, version, numFonts = struct.unpack(ttcHeaderFormat, data[:ttcHeaderSize])
	offsetdata = data[ttcHeaderSize:]
	print "Version: %s.  numFonts: %s." % (version, numFonts)
	
	fontList = []
	i = 0
	while i < numFonts:
		offset = struct.unpack(offsetFormat, offsetdata[:offsetSize])[0]
		print "font %s offset: %s/0x%08X." % (i, offset,offset),
		fontEntry = readFontFile(offset, data)
		fontList.append(fontEntry)
		offsetdata = offsetdata[offsetSize:]
		i += 1
		
	if writeOTF:		
		for fontEntry in fontList:
			writeOTFFont(fontEntry)
	print "Done"

if __name__ == "__main__":
	try:
		run(sys.argv[1:])
	except (OTCError), e:
		print e.message

将此文件保存到 Linux 系统中,并运行如下命令

python2.7 otc2otf.py -w PingFang.ttc

此时运行后,将解构 PingFang.ttc 并生成 18 个字体文件和一个字体简介文件(PSNameUndefined.otf)
之后我们用不到这个字体简介文件,在此处删掉即可。

操作完毕后我们即可得到 Linux 可以使用的大陆简体 SC,台湾繁体 TC,香港繁体 HK 三类字体文件

PingFangHK-Light.otf       PingFangSC-Light.otf       PingFangTC-Light.otf
PingFangHK-Medium.otf      PingFangSC-Medium.otf      PingFangTC-Medium.otf
PingFangHK-Regular.otf     PingFangSC-Regular.otf     PingFangTC-Regular.otf
PingFangHK-Semibold.otf    PingFangSC-Semibold.otf    PingFangTC-Semibold.otf
PingFangHK-Thin.otf        PingFangSC-Thin.otf        PingFangTC-Thin.otf
PingFangHK-Ultralight.otf  PingFangSC-Ultralight.otf  PingFangTC-Ultralight.otf

3.3 、重置 Cmap 表

如果你这时候拿到这些字体文件,试图在 Windows 上安装时,会发现无法识别。

这是由于 PingFang 的 CMap 表仅有两个 platformID="0" 的子表(Unicode 平台的 CMap),但是没有 platformID="3" 的子表(Windows 平台的 CMap)而造成的。因此无法被 Windows 识别。

但是由于这两类 CMap 的编码方式完全相同,所以我们可以直接取巧替换掉 platformID="0" 为 platformID="3"

在替换之前,你需要安装 Fonttools 这个工具

yum install python-pip && pip install fonttools

安装好后你可以在字体文件目录中直接运行下面的命令,如果觉得一条条执行麻烦可以统一写在一个 .sh 文件中执行

# PingFangSC
ttx -t cmap PingFangSC-Light.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangSC-Light.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangSC-Light.ttx
ttx -b -m PingFangSC-Light.otf PingFangSC-Light.ttx

ttx -t cmap PingFangSC-Medium.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangSC-Medium.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangSC-Medium.ttx
ttx -b -m PingFangSC-Medium.otf PingFangSC-Medium.ttx

ttx -t cmap PingFangSC-Regular.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangSC-Regular.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangSC-Regular.ttx
ttx -b -m PingFangSC-Regular.otf PingFangSC-Regular.ttx

ttx -t cmap PingFangSC-Semibold.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangSC-Semibold.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangSC-Semibold.ttx
ttx -b -m PingFangSC-Semibold.otf PingFangSC-Semibold.ttx

ttx -t cmap PingFangSC-Thin.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangSC-Thin.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangSC-Thin.ttx
ttx -b -m PingFangSC-Thin.otf PingFangSC-Thin.ttx

ttx -t cmap PingFangSC-Ultralight.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangSC-Ultralight.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangSC-Ultralight.ttx
ttx -b -m PingFangSC-Ultralight.otf PingFangSC-Ultralight.ttx

# PingFangTC
ttx -t cmap PingFangTC-Light.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangTC-Light.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangTC-Light.ttx
ttx -b -m PingFangTC-Light.otf PingFangTC-Light.ttx

ttx -t cmap PingFangTC-Medium.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangTC-Medium.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangTC-Medium.ttx
ttx -b -m PingFangTC-Medium.otf PingFangTC-Medium.ttx

ttx -t cmap PingFangTC-Regular.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangTC-Regular.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangTC-Regular.ttx
ttx -b -m PingFangTC-Regular.otf PingFangTC-Regular.ttx

ttx -t cmap PingFangTC-Semibold.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangTC-Semibold.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangTC-Semibold.ttx
ttx -b -m PingFangTC-Semibold.otf PingFangTC-Semibold.ttx

ttx -t cmap PingFangTC-Thin.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangTC-Thin.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangTC-Thin.ttx
ttx -b -m PingFangTC-Thin.otf PingFangTC-Thin.ttx

ttx -t cmap PingFangTC-Ultralight.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangTC-Ultralight.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangTC-Ultralight.ttx
ttx -b -m PingFangTC-Ultralight.otf PingFangTC-Ultralight.ttx

# PingFangHK
ttx -t cmap PingFangHK-Light.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangHK-Light.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangHK-Light.ttx
ttx -b -m PingFangHK-Light.otf PingFangHK-Light.ttx

ttx -t cmap PingFangHK-Medium.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangHK-Medium.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangHK-Medium.ttx
ttx -b -m PingFangHK-Medium.otf PingFangHK-Medium.ttx

ttx -t cmap PingFangHK-Regular.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangHK-Regular.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangHK-Regular.ttx
ttx -b -m PingFangHK-Regular.otf PingFangHK-Regular.ttx

ttx -t cmap PingFangHK-Semibold.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangHK-Semibold.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangHK-Semibold.ttx
ttx -b -m PingFangHK-Semibold.otf PingFangHK-Semibold.ttx

ttx -t cmap PingFangHK-Thin.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangHK-Thin.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangHK-Thin.ttx
ttx -b -m PingFangHK-Thin.otf PingFangHK-Thin.ttx

ttx -t cmap PingFangHK-Ultralight.otf
sed -i '''s/platformID="0" platEncID="3"/platformID="3" platEncID="1"/g' PingFangHK-Ultralight.ttx
sed -i '''s/platformID="0" platEncID="4"/platformID="3" platEncID="10"/g' PingFangHK-Ultralight.ttx
ttx -b -m PingFangHK-Ultralight.otf PingFangHK-Ultralight.ttx

命令执行需要一段时间,运行完毕后你可以看到当前文件夹多了 ttx 文件 和带有 #1 后缀的 otf 文件
这些带有 #1 后缀的 otf 文件就是可以在 Windows 上安装的字体文件了


4 、参考文章

蘋方移植 [ 链接 ]

CMap 表相关修改技术简要指南 [ 链接 ]

介绍和构建 OpenType 集合(OTC)[ 链接 ]

关于使用 OS X 10.11 的新中文字体的注意事项 [ 链接 ]

StarryVoid

Have a good time