-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 83dd1c3
Showing
7 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
build/ | ||
dist/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
MIT License | ||
|
||
Copyright (c) 2022 Asvel | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# monoback | ||
|
||
各位有没有遇到过发现了一款心仪的编程字体,但是它和常用中文字体的字符宽度比不是恰好 1:2,不得不忍痛放弃它或者放弃中英文对齐的情况呢?在许多编辑器已经良好支持字体 fallback 的现在,为想用的等宽字体生成一份中文 fallback 字体不失为一种简单灵活的解决方案。 | ||
|
||
相较于成品整合型字体,这种方案: | ||
* 可以自由选择使用的字体。 | ||
* 主字体的全部特性一定能被完整保留,并且可以随时更新版本。 | ||
* 不需要相关字体“再发行”级别的授权许可。 | ||
|
||
但是: | ||
* 需要使用环境支持字体 fallback。 | ||
* 只适配了字符宽度,中英文整体视觉效果与人工精调相比存在差距。 | ||
|
||
另外,英文字体较为美观的宽高比大概在 6:5 左右,而汉字通常是 1:1 的正方形,要让它们严格对齐有一方做出取舍不可避免(或者两方各取舍一点),本方案是一个只牺牲中文部分观感(字间距偏大)的方案,适合用于程序代码这种大段英文夹杂零星中文的场景,不太适合中英文混排为主的场景。~~当然你也可以选择 fallback 到比较扁的中文字体。~~ 总之,这并非一个能够提供完美观感的解决方案,但是它可以让你在使用任意编程字体的同时保持中英文对齐。 | ||
|
||
|
||
## 安装 | ||
|
||
从 PyPI 安装(需要 Python 3.7+): | ||
``` | ||
pip install monoback | ||
``` | ||
|
||
或者从[发布页面](https://github.com/Asvel/monoback/releases/latest)下载独立可执行版本。(仅限 Windows) | ||
|
||
|
||
## 用法 | ||
|
||
``` | ||
monoback <等宽字体文件> <fallback字体文件> [<输出文件>] | ||
``` | ||
|
||
然后安装生成的字体并在你的编辑器设置里指定它为 fallback 字体,例如: | ||
* 在 VSCode 中,设置 `Editor: Font Family` 为字符串 `'主等宽字体名', '生成的字体名'`。 | ||
* 在 JetBrains 产品中, 设置 `编辑器 > 字体 > 版式设置 > 回滚字体` 为生成的字体。 | ||
|
||
|
||
## License | ||
|
||
monoback is licensed under the [MIT license](LICENSE.txt). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# monoback | ||
|
||
Generate aligned CJK fallback font for your favorite monospaced font. | ||
|
||
[中文介绍](README-CN.md) | ||
|
||
|
||
## Installation | ||
|
||
Install from PyPI (requires Python 3.7+): | ||
``` | ||
pip install monoback | ||
``` | ||
|
||
Or download standalone executable from [release page](https://github.com/Asvel/monoback/releases/latest). (Windows only) | ||
|
||
|
||
## Usage | ||
|
||
``` | ||
monoback <monospaced-font-file> <fallback-font-file> [<output-file>] | ||
``` | ||
|
||
Then install the generated font and configure it as fallback font in your editor settings, examples: | ||
* In VSCode, set `Editor: Font Family` to string `'main monospaced font name', 'generated font name'`. | ||
* In JetBrains products, set `Editor > Font > Typography Settings > Fallback font` to generated font. | ||
|
||
|
||
## License | ||
|
||
monoback is licensed under the [MIT license](LICENSE.txt). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
"""Generate aligned CJK fallback font for your favorite monospaced font.""" | ||
|
||
import argparse | ||
import logging | ||
from fontTools import ttLib | ||
|
||
__version__ = '0.1.0' | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def processFont(ttFont, ratio): | ||
if 'CFF ' in ttFont.reader.tables or 'CFF2' in ttFont.reader.tables: | ||
raise RuntimeError("OpenType/CFF font is not supported, view https://github.com/Asvel/monoback/issues/1 for detail.") | ||
|
||
halfWidth = round(ttFont['head'].unitsPerEm * ratio) | ||
fullWidth = round(ttFont['head'].unitsPerEm * ratio * 2) | ||
|
||
# give a new unique name | ||
# a font could fallback with different monospaced fonts, we need distinguish them | ||
name = ttFont['name'] | ||
platformIDs = {n.platformID for n in name.names} | ||
names = [] | ||
def addName(string, nameId): | ||
if nameId == 1 and len(string) > 31: | ||
# if family name longer than 31 characters, behaviors become weird under some render engines | ||
# preserve first three characters for sorting, and more suffixes for maximum distinction | ||
string = f'{string[:3]}..{string[-26:]}' | ||
if 1 in platformIDs: names.append(ttLib.tables._n_a_m_e.makeName(string, nameId, 1, 0, 0)) | ||
if 3 in platformIDs: names.append(ttLib.tables._n_a_m_e.makeName(string, nameId, 3, 1, 1033)) | ||
if 0 in platformIDs: names.append(ttLib.tables._n_a_m_e.makeName(string, nameId, 0, 3, 0)) | ||
nameSuffix = f'Mono{fullWidth}' | ||
nameHyphen = '-' if '-' in name.getBestFamilyName() else ' ' | ||
addName(f'{name.getBestFamilyName()}{nameHyphen}{nameSuffix}', 1) | ||
addName(f'{name.getBestSubFamilyName()}', 2) | ||
addName(f'{name.getDebugName(3)}{nameHyphen}{nameSuffix}', 3) | ||
addName(f'{name.getBestFullName()}{nameHyphen}{nameSuffix}', 4) | ||
addName(f'{name.getDebugName(5)};monoback {__version__}', 5) | ||
addName(f'{name.getDebugName(6)}-{nameSuffix}', 6) | ||
name.names = names + [n for n in name.names if n.nameID == 0 or 7 <= n.nameID <= 15] | ||
logger.info(name.getBestFamilyName()) | ||
|
||
# change width of every glyph | ||
ttFont['hhea'].advanceWidthMax = fullWidth | ||
hmtx = ttFont['hmtx'].metrics | ||
for k, (width, lsb) in hmtx.items(): | ||
destLsb = lsb + round((fullWidth - width) / 2) | ||
hmtx[k] = (fullWidth, destLsb) | ||
|
||
# indicate render engine to render in monospace mode, and align base on halfWidth | ||
ttFont['OS/2'].xAvgCharWidth = halfWidth | ||
ttFont['OS/2'].panose.bProportion = 9 | ||
ttFont['post'].isFixedPitch = 1 | ||
if 3 in platformIDs and ttFont['OS/2'].ulCodePageRange1 & (0b00111110 << 16) == 0: | ||
ttFont['OS/2'].ulCodePageRange1 |= (0b00111110 << 16) | ||
|
||
# discard pre-determined width since we expect render engine to decide glyph width as above | ||
ttFont.reader.tables.pop('hdmx', None) | ||
ttFont.reader.tables.pop('LTSH', None) | ||
ttFont['head'].flags &= ~(1 << 4) | ||
|
||
# kerning is not need since all glyph should display at fixed spacing | ||
ttFont.reader.tables.pop('kern', None) | ||
|
||
# warning for variable font | ||
if 'fvar' in ttFont.reader.tables: | ||
logger.warning("warning: the fallback font seem to be a variable font, this tool may not handle it properly, " + | ||
"and most code editors can't handle variation very well either, we recommend using a non-variable one.") | ||
|
||
return nameSuffix | ||
|
||
|
||
def main(): | ||
parser = argparse.ArgumentParser(description=__doc__, epilog=f'monoback {__version__} (https://github.com/Asvel/monoback)') | ||
parser.add_argument('monospaced', help="path of base monospaced font") | ||
parser.add_argument('fallback', help="path of source fallback font") | ||
parser.add_argument('output', nargs='?', help="path of patched fallback font") | ||
args = parser.parse_args() | ||
logging.basicConfig(level=logging.INFO, format='%(message)s') | ||
|
||
ttFont = ttLib.TTFont(args.monospaced, fontNumber=0) | ||
assert ttFont['hmtx'].metrics['i'][0] == ttFont['hmtx'].metrics['m'][0], f"{args.monospaced} is not monospaced." | ||
ratio = ttFont['hmtx'].metrics['i'][0] / ttFont['head'].unitsPerEm | ||
ttFont.close() | ||
|
||
nameSuffix = None | ||
if args.fallback.lower().endswith('.ttc'): | ||
ttFont = ttLib.ttCollection.TTCollection(args.fallback) | ||
logger.info("Generated font names:") | ||
for ttFont_ in ttFont.fonts: | ||
nameSuffix = processFont(ttFont_, ratio) | ||
else: | ||
ttFont = ttLib.TTFont(args.fallback) | ||
logger.info("Generated font name:") | ||
nameSuffix = processFont(ttFont, ratio) | ||
|
||
if args.output is None: | ||
rest, _, ext = args.fallback.rpartition('.') | ||
args.output = f'{rest}-{nameSuffix}.{ext}' | ||
ttFont.save(args.output) | ||
|
||
ttFont.close() | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# -*- mode: python ; coding: utf-8 -*- | ||
|
||
|
||
block_cipher = None | ||
|
||
|
||
a = Analysis( | ||
['monoback.py'], | ||
pathex=[], | ||
binaries=[], | ||
datas=[], | ||
hiddenimports=[], | ||
hookspath=[], | ||
hooksconfig={}, | ||
runtime_hooks=[], | ||
excludes=[], | ||
win_no_prefer_redirects=False, | ||
win_private_assemblies=False, | ||
cipher=block_cipher, | ||
noarchive=False, | ||
) | ||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) | ||
|
||
exe = EXE( | ||
pyz, | ||
a.scripts, | ||
a.binaries, | ||
a.zipfiles, | ||
a.datas, | ||
[], | ||
name='monoback', | ||
debug=False, | ||
bootloader_ignore_signals=False, | ||
strip=False, | ||
upx=True, | ||
upx_exclude=[], | ||
runtime_tmpdir=None, | ||
console=True, | ||
disable_windowed_traceback=False, | ||
argv_emulation=False, | ||
target_arch=None, | ||
codesign_identity=None, | ||
entitlements_file=None, | ||
icon='NONE', | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
[build-system] | ||
requires = ["flit_core >=3.2,<4"] | ||
build-backend = "flit_core.buildapi" | ||
|
||
[project] | ||
name = "monoback" | ||
authors = [{name = "Asvel", email = "[email protected]"}] | ||
readme = "README.md" | ||
requires-python = "~=3.7" | ||
license = {file = "LICENSE.txt"} | ||
classifiers = [ | ||
"Development Status :: 4 - Beta", | ||
"Environment :: Console", | ||
"Intended Audience :: End Users/Desktop", | ||
"Programming Language :: Python :: 3", | ||
"License :: OSI Approved :: MIT License", | ||
"Operating System :: OS Independent", | ||
"Topic :: Text Processing :: Fonts", | ||
] | ||
dependencies = [ | ||
"fonttools >=4", | ||
] | ||
dynamic = ["version", "description"] | ||
|
||
[project.urls] | ||
Home = "https://github.com/Asvel/monoback" | ||
|
||
[project.scripts] | ||
monoback = "monoback:main" |