Skip to content

Commit

Permalink
First commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
Asvel committed Sep 8, 2022
0 parents commit 83dd1c3
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
build/
dist/
21 changes: 21 additions & 0 deletions LICENSE.txt
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.
40 changes: 40 additions & 0 deletions README-CN.md
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).
31 changes: 31 additions & 0 deletions README.md
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).
106 changes: 106 additions & 0 deletions monoback.py
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()
45 changes: 45 additions & 0 deletions monoback.spec
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',
)
29 changes: 29 additions & 0 deletions pyproject.toml
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"

0 comments on commit 83dd1c3

Please sign in to comment.