Add bundled bus fonts and shared font favorites

This commit is contained in:
2026-10-12 15:00:40 +08:00
parent c9dd03c23b
commit 4be74a34f9
181 changed files with 2078575 additions and 76 deletions
+1 -1
View File
@@ -1 +1 @@
2026-10-10T14:17+08:00
2026-10-12T14:25+08:00
@@ -16,6 +16,7 @@ from uuid import UUID, uuid4
from app.animations.constants import DEFAULT_DURATION_MS, MAX_DURATION_MS, MIN_DURATION_MS
from app.display.text_renderer import FontResolver
from app.persistence import atomic_write_bytes
from app.fonts.render_cache import cache_stale, stamp_cache, STAMP_SUFFIX, cached_versioned_url
from app.templates.store import (
TemplateConflictError, TemplateError, TemplateNotFoundError,
TemplateStorageFullError, TemplateValidationError,
@@ -166,11 +167,12 @@ class AnimationStore:
def _write_thumbnail(self, animation_id: str, frame: dict[str, Any]) -> None:
self._thumb_dir(animation_id).mkdir(parents=True, exist_ok=True)
path = self._thumb_dir(animation_id) / frame["thumbnail"]
if path.exists():
return
scene = frame.get("scene") or self._load_scene(animation_id, frame)
if path.exists() and not cache_stale(path, scene, self._font_resolver):
return
try:
atomic_write_bytes(path, render_scene_thumbnail(scene, font_resolver=self._font_resolver))
stamp_cache(path, scene, self._font_resolver)
except OSError as exc:
self._translate(exc)
@@ -284,10 +286,10 @@ class AnimationStore:
"updated_at": str(raw.get("updated_at") or ""),
"digest": digest, "thumbnail": thumbnail, "scene_file": scene_file,
}
if self._load_scene(expected_id, frame)["version"] != 2:
checked_scene = self._load_scene(expected_id, frame)
if checked_scene["version"] != 2:
raise TemplateValidationError("animation schema v3 requires scene v2")
if not (self._thumb_dir(expected_id) / thumbnail).is_file():
self._write_thumbnail(expected_id, frame)
self._write_thumbnail(expected_id, {**frame, "scene": checked_scene})
frames.append(frame)
return {
"id": expected_id, "name": _name(document.get("name")),
@@ -315,6 +317,8 @@ class AnimationStore:
for path in directory.iterdir():
if path.is_file() and path.name not in expected and pattern.fullmatch(path.name):
path.unlink(missing_ok=True)
elif path.is_file() and path.name.endswith(STAMP_SUFFIX) and pattern.fullmatch(path.name[:-len(STAMP_SUFFIX)]) and path.name[:-len(STAMP_SUFFIX)] not in expected:
path.unlink(missing_ok=True)
def _all(self) -> list[dict[str, Any]]:
return [
@@ -386,7 +390,8 @@ class AnimationStore:
return all(r["name"].casefold() != checked.casefold() for r in self._all())
def thumbnail_url(self, animation_id: str, frame: dict[str, Any]) -> str:
return f"/api/animations/{animation_id}/frames/{frame['id']}/thumbnail?v={frame['digest']}"
return cached_versioned_url(f"/api/animations/{animation_id}/frames/{frame['id']}/thumbnail?v={frame['digest']}",
self._thumb_dir(animation_id) / frame["thumbnail"])
def _metadata(self, record: dict[str, Any]) -> dict[str, Any]:
revision = _revision(record)
@@ -0,0 +1,110 @@
[BoutiqueBitmap7x7]
2020-2026《字言字型》Cen-cyun, Liu. Luke Liu.
https://fontspeech.blogspot.com/
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
此字型是免費的。
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
[MisakiGothic]
Copyright (C) 2002-2019 Num Kadoma
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
これらのフォントはフリー(自由な)ソフトウエアです。
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
[观致]
这是日文字体Misaki基础上补充的
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
Copyright (c) 2019–2025 Lee Minseo (quiple@quiple.dev)
This font software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,258 @@
===============================================================================
8×8 ドット日本語フォント「美咲フォント」
(2021-05-05 版)
Copyright(C) 2002-2021 Num Kadoma
===============================================================================
-------------------------------------------------------------------------------
概要
-------------------------------------------------------------------------------
 美咲フォントは 8×8 ドットの日本語ビットマップフォントです。
 JIS第一・第二水準をサポートしています。
 一部の記号を除いた全ての文字は 7×7 ドットの範囲に収まっているため、
 文字同士を隣接させても行間・字間が確保できます。
 フォント名は制作のきっかけとなった美咲 礼威氏からいただきました。
-------------------------------------------------------------------------------
バリエーション
-------------------------------------------------------------------------------
・「美咲明朝」は、美咲ゴシックの非漢字部を明朝風の字形に差し替えたものです。
・「美咲ゴシック第2」は美咲ゴシックをベースに、半角文字は縦 7 px、
 全角仮名は横 7 px をいっぱいに使う字形に差し替えたものです。
 各機種用のアーカイブに、差替え済みのフォントファイルを同梱してあります。
-------------------------------------------------------------------------------
ライセンス
-------------------------------------------------------------------------------
 These fonts are free softwares.
 Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
 THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
 これらのフォントはフリー(自由な)ソフトウエアです。
 あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
-------------------------------------------------------------------------------
制作履歴
-------------------------------------------------------------------------------
■2021-05-05
・文字を追加
 共通: ␣
・字形を修正・変更
 共通: №
 ゴシック第2: プヴ
・(TTF) ビットマップを埋込み
■2019-10-19
・ドキュメントの文字コードを UTF-8N、改行コードを LF に統一
・(TTF, BDF) 記号を追加:
  ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉▔▕╭╮╰╯♠♥♦♣╱╲╳♤♡♢♧
■2019-08-18
・各ファイルのパーミッションから、不要な実行ビットを除去 (フォントの変更はなし)
■2019-06-03
・新フォント「美咲ゴシック第2」を追加。英語名「MisakiGothic2nd」
・「令和」の合字を追加
・各種字形の修正
・(E500) ファイル名のつけ方を変更して配布を再開
・(BDF) 一部仕様を変更して配布を再開
  エンコーディングは ISO 10646-1 のみとし、全ての文字を 1 ファイルに収録
  XLFD 変更
  BITMAP を文字の上下左右端の空白を削った形とし、BBX で位置を指定する形に修正
■2019-02-03a
・制作履歴における、変更した文字の抜け (ゴシックの「と」「ね」「ゐ」) を修正
■2019-02-03
・ゴシック・明朝共通の変更: &,.‘’()〔〕[]{}〈〉【】°′″№0索二
  半角 (「&」字形変更)
  記号類 (主に括弧類の位置調整)
  数字 (ゼロにスラッシュを追加)
  漢字 (「索」「二」の字形変更)
・ゴシックのみの変更: 69CGOQΟОСЭぐぜとどねばぶゐガグゾダバヴ
  数字 (「6」「9」の先端を伸ばす形に変更)
  アルファベット (オーなどの丸み変更)
  仮名 (主に濁点/半濁点つき文字の調整)
・明朝のみの変更: ぐでとどなぶウグケサザソゾダドワヱヴЁ
  仮名 (各種字形変更)
  キリル (「Ё」の字形変更)
■2015-04-10
・誤字になっていた「喪」を修正
・(TTF) ウェイトを「Regular」に変更
  一部の記号を追加、変更
  バージョン番号を「yyyy.mmdd」の形に変更
  その他、パラメータを見直し
  埋込みビットマップ版の同梱を取り止め
■2012-06-03
・正式公開初版
■2012-03-31
・各種ファイル名、ドキュメントの見直し
・BDF 版以外のアーカイブ形式を zip に変更
・(TTF) アウトライン版の正式採用、Mac におそらく対応
・(FONTX) 4X8.FNT の 00h~1Fh に DOS/V 罫線を追加
■2011-03-08
・(TTF-Outline) 生成手順の改善に伴い periodβ11 作成・公開
■2010-03-07
・(BDF) FOUNDRY, POINT_SIZE, AVERAGE_WIDTH, SWIDTH, XLFD, 各ファイル名の変更
■2006-10-01
・(TTF-Outline) 試作版配布開始
■2006-04-01
・(PNG) 配布開始
■2006-03-26
・(E500) 13 区追加版・軽量版 (第一水準のみ) 同梱開始
  フォントファイルの命名規則変更
・(BDF) 美咲明朝の配布形態をゴシックとの差分から実ファイル同梱へ
  BITMAP の記述方法を修正 (16 進 4 桁から 2 桁へ)
・(FONTX) 配布再開
■2005-07-20
・アーカイブの命名規則を変更
・(TTF, BDF) 著作権表記を変更
・(BDF) 1 バイト文字フォントのファイル名をそれぞれ 4x8.bdf, 4x8_8859.bdf に
■2004-09-12
・(TTF, BDF) 13 区の文字追加
■2004-06-24
・フォントの名称変更
・(美咲フォントを美咲ゴシック、ファミリーの総称を美咲フォントに)
■2004-05-09
・美咲明朝の同梱開始
■2004-04-19
・半濁点の処理を変更
■2004-04-11
・3 フォント同時編集ツール「"SCRNJPN.FNT" Editor」完成
■2004-02-09
・濁点の処理を変更
■2003-09-21
・制作履歴の日付ミスを修正 (美咲氏 Thanx!)
■2003-06-04
・ライセンスに関する記述を変更
■2003-05-04
・(BDF) 3x7_8859.bdf の CHARS の値を修正
・(BDF) 3x7_8859.bdf のファイル名が 3X7_8859.bdf になっていたのを修正
■2003-03-26
・マニュアルをフォントの説明と各形式の説明に分離
■2003-01-02
・各部のフォント間での統一化作業を本格的に開始
■2002-06-10
・JIS X 0208-1983 での文字の変更、JIS X 0208-1990 での文字の追加に対応
■2002-06-06
・未定義文字のフォントを 3×3 の ■ に
■2002-06-01
・第二水準完成
■2002-05-22
・制作再開
■2002-05-21
・フォント作成専用ツール「'SCRNJPN.FNT' Editor」完成
■2002-05-11
・E650 暴走により第二水準中の 600 文字近くが消滅→制作中断
■2002-04-初
・第一水準完成
■2002-03-中
・制作開始
-------------------------------------------------------------------------------
バージョン情報
-------------------------------------------------------------------------------
■2021-05-05
・美咲フォント 2021-05-05 版 (E500/BDF/TTF/PNG)
■2019-10-19
・美咲フォント 2019-10-19 版 (E500/BDF/TTF/PNG)
■2019-08-18
・美咲フォント 2019-06-03a 版 (E500/BDF/TTF/PNG)
■2019-06-03
・美咲フォント 2019-06-03 版 (E500/BDF/TTF/PNG)
■2019-02-03
・美咲フォント 2019-02-03 版 (TTF/PNG)
■2015-04-10
・美咲フォント 2015-04-10 版 (TTF/PNG)
■2012-06-03
・美咲フォント 2012-06-03 版 (E500/BDF/TTF/Ruputer/FONTX/PNG)
■2012-03-31
・美咲フォント periodβ12 (E500/BDF/TTF/Ruputer/FONTX/PNG)
■2010-03-07
・美咲フォント periodβ11a (BDF)
■2008-06-03
・美咲フォント periodβ11 (E500/BDF/TTF/Ruputer/FONTX/PNG/TTF-Outline)
■2006-03-26
・美咲フォント periodβ10 (E500/BDF/TTF/Ruputer/FONTX/PNG/TTF-Outline)
■2005-07-20
・美咲フォント periodβ9 (E500/BDF/TTF/Ruputer)
■2004-05-09
・美咲フォント periodβ8 (E500/BDF/TTF/Ruputer)
■2003-09-21
・美咲フォント periodβ7 (E500/BDF/TTF/Ruputer)
■2003-06-04
・美咲フォント periodβ6 (E500/BDF/TTF/Ruputer)
■2003-05-04
・美咲フォント periodβ5 (E500/BDF/TTF/Ruputer)
■2003-03-26
・美咲フォント periodβ4 (E500/BDF/TTF/Ruputer/FONTX)
■2002-12-14
・美咲フォント periodβ3 (E500)
■2002-06-18
・美咲フォント periodβ2 (E500)
■2002-06-03
・美咲フォント periodβ1 (E500)
-------------------------------------------------------------------------------
連絡先
-------------------------------------------------------------------------------
 門真 なむ (Num Kadoma)
 ・Twitter: @num_kadoma
 ・Website: http://littlelimit.net/
@@ -0,0 +1,92 @@
Copyright (c) 2023-2024 Mark Li (itmarkibfb@gmail.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1 @@
COPYRIGHT "Copyright (C) 1998-2026 Roman Czyborra, Paul Hardy, Qianqian Fang, Andrew Miller, Johnnie Weaver, David Corbett, Ælla Chiana Moskopp, Rebecca Bettencourt, Minseo Lee, Ho-Seok Ee, et al. License: SIL Open Font License version 1.1 and GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html> with the GNU Font Embedding Exception."
@@ -0,0 +1,476 @@
LICENSE
-------
The source code for everything except the compiled fonts in this current
release is licensed as follows:
License for this current distribution of program source
files (i.e., everything except the fonts) is released under
the terms of the GNU General Public License version 2,
or (at your option) a later version.
See the section below for a copy of the GNU General Public License
version 2.
The license for the compiled fonts is covered by the SIL Open Font License
version 1.1 and by the above GPL terms with the GNU font embedding exception,
as follows:
As a special exception, if you create a document which uses this font,
and embed this font or unaltered portions of this font into the document,
this font does not by itself cause the resulting document to be covered
by the GNU General Public License. This exception does not however
invalidate any other reasons why the document might be covered by the
GNU General Public License. If you modify this font, you may extend
this exception to your version of the font, but you are not obligated
to do so. If you do not wish to do so, delete this exception statement
from your version.
See "http://www.gnu.org/licenses/gpl-faq.html#FontException" and
"https://scripts.sil.org/OFL" for more details.
GPL VERSION 2
-------------
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
SIL Open Font License version 1.1
---------------------------------
Copyright (c) <dates>, <Copyright Holder> (<URL|email>),
with Reserved Font Name <Reserved Font Name>.
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>),
with Reserved Font Name <additional Reserved Font Name>.
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,90 @@
The SIL Open Font License version 1.1 is copied below, and is also
available with a FAQ at http://scripts.sil.org/OFL.
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,21 @@
{
"version": "18.0.01",
"license_choice": "OFL-1.1",
"files": [
{
"file": "unifont-18.0.01.bdf.gz",
"url": "https://unifoundry.com/pub/unifont/unifont-18.0.01/font-builds/unifont-18.0.01.bdf.gz",
"sha256": "2989e0211030d8219fd931aba599fc95c86bc327ce1423848ef949783b208257"
},
{
"file": "OFL-1.1.txt",
"url": "https://unifoundry.com/OFL-1.1.txt",
"sha256": "869692af094c57fb7258c57fe26820c759319603321d0ffeb278de3651763ded"
},
{
"file": "LICENSE.txt",
"url": "https://unifoundry.com/LICENSE.txt",
"sha256": "1e74cb82bf476843e97c2596297b04219b1a7e51f7238944a8c031cb9401fa87"
}
]
}
@@ -0,0 +1,96 @@
Fusion Pixel Font
https://github.com/TakWolf/fusion-pixel-font
Copyright (c) 2022, TakWolf (https://takwolf.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,39 @@
{
"version": 2,
"fonts": [
{
"id": "builtin:walnut-bus-8x16",
"file": "SimilarBus8x16.ttf",
"sha256": "aba652e330975ff3db2f41a2795a2d0478806ed83c52197acdbee85291cf5111",
"label": "类似公交窄体(推荐16px)",
"family": "类似公交窄体(推荐16px)",
"style": "Regular",
"glyph_count": 6884,
"recommended_size": 16,
"cell": [
8,
16
]
},
{
"id": "builtin:walnut-bus-16x32",
"file": "SimilarBus16x32.ttf",
"sha256": "b1d4035f2fa9dfd839f1b573b994cbec245f041c4a0a62ee9ef1a7c7b1bfa126",
"label": "类似公交窄体(推荐32px)",
"family": "类似公交窄体(推荐32px)",
"style": "Regular",
"glyph_count": 6884,
"recommended_size": 32,
"cell": [
16,
32
]
}
],
"obsolete": [
"d38bf29d6408a6f731865a1ca760a80b5156ebf6ab96d5ad8d649df28163564c"
],
"aliases": {
"user:d38bf29d6408a6f731865a1ca760a80b5156ebf6ab96d5ad8d649df28163564c:0": "builtin:walnut-bus-8x16"
}
}
@@ -0,0 +1,64 @@
"""Version only thumbnails that use a bundled font; preserve scene revisions."""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from app.persistence import atomic_write_bytes
STAMP_SUFFIX = ".font-version"
def scene_font_version(scene: dict, resolver) -> str | None:
owner = getattr(resolver, "__self__", None)
version_for = getattr(owner, "render_version", None)
if version_for is None:
return None
elements = scene.get("elements", []) if scene.get("version") == 1 else [
element for layer in scene.get("layers", []) if layer.get("type") == "text"
for element in layer.get("elements", [])
]
versions = sorted({version for element in elements
if (version := version_for(element.get("font", "default")))})
if not versions:
return None
return hashlib.sha256(json.dumps(versions, separators=(",", ":")).encode("ascii")).hexdigest()[:16]
def stamp_path(path: Path) -> Path:
return path.with_name(path.name + STAMP_SUFFIX)
def cache_stale(path: Path, scene: dict, resolver) -> bool:
version = scene_font_version(scene, resolver)
if version is None:
return False
try:
return stamp_path(path).read_text(encoding="ascii").strip() != version
except (OSError, UnicodeError):
return True
def stamp_cache(path: Path, scene: dict, resolver) -> None:
version = scene_font_version(scene, resolver)
stamp = stamp_path(path)
if version is not None:
atomic_write_bytes(stamp, (version + "\n").encode("ascii"))
else:
stamp.unlink(missing_ok=True)
def versioned_url(url: str, scene: dict, resolver) -> str:
version = scene_font_version(scene, resolver)
return url + f"&font_v={version}" if version else url
def cached_versioned_url(url: str, path: Path) -> str:
try:
version = stamp_path(path).read_text(encoding="ascii").strip()
except (OSError, UnicodeError):
return url
if len(version) == 16 and all(ch in "0123456789abcdef" for ch in version):
return url + f"&font_v={version}"
return url
+124 -1
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import errno
import hashlib
import json
import logging
import os
import subprocess
@@ -15,6 +16,7 @@ from uuid import uuid4
from fontTools.ttLib import TTCollection, TTFont, TTLibError
from app.display.text_renderer import DEFAULT_FONT_CANDIDATES, FontFace
from app.persistence import atomic_write_bytes
logger = logging.getLogger(__name__)
@@ -186,10 +188,102 @@ class FontCatalog:
self.root.mkdir(parents=True, exist_ok=True)
if os.name != "nt":
self.root.chmod(0o750)
self._favorite_path = self.root / "favorites.json"
self._favorite_ids = self._load_favorites()
self._cleanup_upload_temporaries()
self._builtin = self._install_builtins()
self._system_catalog = self._system_faces()
self.refresh()
def _load_favorites(self) -> list[str]:
if not self._favorite_path.exists():
atomic_write_bytes(self._favorite_path, b'{"schema_version":1,"favorite_ids":[]}\n')
return []
try:
record = json.loads(self._favorite_path.read_text(encoding="utf-8"))
if (not isinstance(record, dict) or set(record) != {"schema_version", "favorite_ids"}
or type(record["schema_version"]) is not int or record["schema_version"] != 1):
raise ValueError("invalid favorites schema")
identifiers = record["favorite_ids"]
if (not isinstance(identifiers, list)
or any(not isinstance(value, str) or not value or len(value) > 512 for value in identifiers)
or len(set(identifiers)) != len(identifiers)):
raise ValueError("invalid favorites identifiers")
return identifiers
except (ValueError, UnicodeError) as exc:
raise FontValidationError("invalid font favorites; original file preserved") from exc
def set_favorite(self, identifier: str, favorite: bool) -> list[str]:
with self._lock:
if favorite and not any(item["id"] == identifier for item in self._items):
raise KeyError(identifier)
updated = [value for value in self._favorite_ids if value != identifier]
if favorite:
updated.append(identifier)
if set(updated) == set(self._favorite_ids):
return list(self._favorite_ids)
content = json.dumps({"schema_version": 1, "favorite_ids": updated}, ensure_ascii=False).encode("utf-8") + b"\n"
atomic_write_bytes(self._favorite_path, content)
self._favorite_ids = updated
return list(updated)
def _install_builtins(self) -> dict[str, tuple[dict, FontFace]]:
bundle = Path(__file__).resolve().parent / "bundled"
manifest = json.loads((bundle / "manifest.json").read_text(encoding="utf-8"))
expected = {"builtin:walnut-bus-8x16": "SimilarBus8x16.ttf", "builtin:walnut-bus-16x32": "SimilarBus16x32.ttf"}
entries = manifest.get("fonts", [])
if manifest.get("version") != 2 or len(entries) != 2 or {item["id"]: item["file"] for item in entries} != expected:
raise FontValidationError("invalid bundled font registration")
self._manifest = manifest
# Validate the entire bundle before modifying persisted resources.
for item in entries:
path = bundle / item["file"]
if hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]:
raise FontValidationError("bundled font checksum mismatch")
_inspect_font_file(path, require_unicode=True)
installed = {item["id"]: (item, self._install_face(bundle, item)) for item in entries}
# Only retire the exact previous built-in asset; never glob user fonts.
for digest in manifest.get("obsolete", []):
if len(digest) != 64 or any(ch not in "0123456789abcdef" for ch in digest):
raise FontValidationError("invalid obsolete font registration")
old = self.root / _USER_FONT_NAME.format(digest=digest)
if old.exists():
if old.is_symlink() or hashlib.sha256(old.read_bytes()).hexdigest() != digest:
raise FontValidationError("obsolete font checksum mismatch; file preserved")
old.unlink()
_sync_directory(self.root)
return installed
def _install_face(self, bundle: Path, manifest: dict) -> FontFace:
payload = (bundle / manifest["file"]).read_bytes()
digest = hashlib.sha256(payload).hexdigest()
if digest != manifest["sha256"]:
raise FontValidationError("bundled font checksum mismatch")
target = self.root / _USER_FONT_NAME.format(digest=digest)
if target.exists():
if hashlib.sha256(target.read_bytes()).hexdigest() != digest:
raise FontValidationError("persisted bundled font checksum mismatch; file preserved")
else:
temporary = self.root / f".upload-{uuid4().hex}.tmp"
try:
with temporary.open("xb") as stream:
if os.name != "nt":
os.chmod(temporary, 0o640)
stream.write(payload)
stream.flush()
os.fsync(stream.fileno())
_inspect_font_file(temporary, require_unicode=True)
# Concurrent initialization must never replace an existing file.
try:
os.link(temporary, target)
except FileExistsError:
if hashlib.sha256(target.read_bytes()).hexdigest() != digest:
raise FontValidationError("persisted bundled font checksum mismatch; file preserved")
_sync_directory(self.root)
finally:
temporary.unlink(missing_ok=True)
return FontFace(str(target.resolve()), 0)
def _cleanup_upload_temporaries(self) -> None:
for path in self.root.glob(".upload-*.tmp"):
if path.is_file() and path.parent == self.root:
@@ -222,6 +316,12 @@ class FontCatalog:
system_items: list[dict[str, str]] = []
imported_items: list[dict[str, str]] = []
warnings: list[str] = []
builtin_digests = {item["sha256"] for item, face in self._builtin.values()}
for manifest, builtin_face in self._builtin.values():
faces[manifest["id"]] = (builtin_face,)
builtin_item = _entry(manifest["id"], manifest["family"], manifest["style"], "system")
builtin_item["label"] = manifest["label"]
system_items.append(builtin_item)
seen_system: set[tuple[str, int]] = set()
for item in self._system_catalog:
@@ -247,7 +347,11 @@ class FontCatalog:
for item in inspected:
identifier = _user_font_id(digest, item.face.index)
faces[identifier] = (item.face,)
imported_items.append(_entry(identifier, item.family, item.style, "imported"))
if digest not in builtin_digests and digest not in self._manifest.get("obsolete", []):
imported_items.append(_entry(identifier, item.family, item.style, "imported"))
for alias, identifier in self._manifest.get("aliases", {}).items():
faces[alias] = faces[identifier]
sort_key = lambda value: (value["family"].casefold(), value["style"].casefold(), value["id"])
automatic = {
@@ -270,6 +374,7 @@ class FontCatalog:
with self._lock:
return {
"items": [dict(item) for item in self._items],
"favorite_ids": list(self._favorite_ids),
"accepted_extensions": list(ACCEPTED_FONT_EXTENSIONS),
"max_upload_bytes": MAX_FONT_UPLOAD_BYTES,
"warnings": list(self._warnings),
@@ -279,6 +384,16 @@ class FontCatalog:
with self._lock:
return self._faces.get(identifier, ())
def render_version(self, identifier: str) -> str | None:
identifier = self._manifest.get("aliases", {}).get(identifier, identifier)
item = self._builtin.get(identifier)
if item:
return item[0]["sha256"]
for manifest, face in self._builtin.values():
if identifier == _user_font_id(manifest["sha256"], 0):
return manifest["sha256"]
return None
async def import_font(
self,
filename: str,
@@ -316,6 +431,11 @@ class FontCatalog:
raise FontValidationError("font file is empty")
inspected = _inspect_font_file(temporary, require_unicode=True)
file_digest = digest.hexdigest()
old_alias = _user_font_id(file_digest, 0)
if file_digest in self._manifest.get("obsolete", []):
temporary.unlink(missing_ok=True)
destination = self._manifest["aliases"][old_alias]
return False, [dict(item) for item in self._items if item["id"] == destination]
target = self.root / _USER_FONT_NAME.format(digest=file_digest)
with self._lock:
created = not target.exists()
@@ -326,6 +446,9 @@ class FontCatalog:
temporary.unlink(missing_ok=True)
self.refresh()
identifiers = {_user_font_id(file_digest, item.face.index) for item in inspected}
for manifest, face in self._builtin.values():
if file_digest == manifest["sha256"]:
identifiers.add(manifest["id"])
imported = [dict(item) for item in self._items if item["id"] in identifiers]
return created, imported
except FontCatalogError:
+15
View File
@@ -134,6 +134,12 @@ def normalize_if_match(value: str | None) -> str:
return candidate
class FontFavoriteRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
id: StrictStr = Field(min_length=1, max_length=512)
favorite: StrictBool
class ConfigUpdate(BaseModel):
orientation: int | None = None
brightness: int | None = None
@@ -991,6 +997,15 @@ def create_app(
def get_fonts() -> dict:
return fonts.document()
@app.put("/api/fonts/favorite")
def update_font_favorite(payload: FontFavoriteRequest) -> dict:
try:
return {"favorite_ids": fonts.set_favorite(payload.id, payload.favorite)}
except KeyError:
raise HTTPException(status_code=404, detail="font is not available")
except OSError:
raise HTTPException(status_code=500, detail="font favorite could not be saved")
@app.post("/api/fonts/import")
async def import_font(request: Request, response: Response, filename: str = "") -> dict:
content_type = request.headers.get("content-type", "").split(";", 1)[0].strip().casefold()
@@ -1,3 +1,5 @@
import { renderUiCopy, unregisterUiCopySubtree } from "./ui-copy-runtime.js";
const SOURCE_ORDER = Object.freeze(["automatic", "imported", "system"]);
const SOURCE_LABELS = Object.freeze({
automatic: "自动字体",
@@ -33,23 +35,29 @@ export function filterFontItems(items, query = "") {
));
}
export function groupFontItems(items) {
return SOURCE_ORDER
export function groupFontItems(items, favoriteIds = []) {
const favorites = new Set(favoriteIds);
return [{ source: "favorites", label: "收藏字体", items: items.filter(item => favorites.has(item.id)) }, ...SOURCE_ORDER
.map((source) => ({
source,
label: SOURCE_LABELS[source],
items: items.filter((item) => item.source === source),
}))
.filter((group) => group.items.length);
.filter((group) => group.items.length)];
}
export function createFontPicker({ input, listbox, onCommit }) {
export function createFontPicker({ input, listbox, onCommit, onFavorite, onOpen }) {
let items = [];
let selectedId = "default";
let visibleItems = [];
let activeIndex = -1;
let enabled = false;
let closeTimer = null;
let favoriteIds = [];
let queryText = "";
let favoriteBusy = false;
let dialogOpen = false;
let restoringFocus = false;
function selectedItem() {
return items.find((item) => item.id === selectedId) || null;
@@ -101,17 +109,31 @@ export function createFontPicker({ input, listbox, onCommit }) {
}
function render(query = "") {
visibleItems = filterFontItems(items, query);
const groups = groupFontItems(visibleItems);
const bounds = input.getBoundingClientRect();
listbox.style.width = `${Math.max(bounds.width, Math.min(440, window.innerWidth - bounds.left - 16))}px`;
queryText = query;
const filtered = filterFontItems(items, query);
const groups = groupFontItems(filtered, favoriteIds);
visibleItems = groups.flatMap(group => group.items);
const nodes = [];
let optionIndex = 0;
for (const group of groups) {
const heading = document.createElement("div");
heading.className = "font-option-group";
heading.textContent = group.label;
if (group.source === "favorites") renderUiCopy("copy.dynamic.font.favorites", heading, "收藏字体");
else heading.textContent = group.label;
heading.setAttribute("role", "presentation");
nodes.push(heading);
if (group.source === "favorites" && !group.items.length) {
const empty = document.createElement("div");
empty.className = "font-options-empty";
renderUiCopy(query.trim() ? "copy.dynamic.font.no_matches" : "copy.dynamic.font.empty", empty, query.trim() ? "没有匹配的收藏字体" : "暂无收藏字体");
nodes.push(empty);
}
for (const item of group.items) {
const row = document.createElement("div");
row.className = "font-option-row";
row.setAttribute("role", "presentation");
const option = document.createElement("div");
option.id = optionId(optionIndex);
option.className = "font-option";
@@ -124,34 +146,96 @@ export function createFontPicker({ input, listbox, onCommit }) {
const source = document.createElement("small");
source.textContent = item.style;
source.setAttribute("aria-hidden", "true");
option.append(label, source);
const star = document.createElement("button");
star.type = "button";
star.className = "font-favorite-button";
const favorite = favoriteIds.includes(item.id);
star.textContent = favorite ? "★" : "☆";
star.setAttribute("aria-label", `${favorite ? "取消收藏" : "收藏"}字体“${item.label}”`);
star.setAttribute("aria-pressed", String(favorite));
star.disabled = favoriteBusy;
star.addEventListener("pointerdown", event => event.stopPropagation());
star.addEventListener("click", event => {
event.stopPropagation();
toggleFavorite(item, favorite);
});
option.append(label);
row.append(option, star, source);
option.addEventListener("pointerdown", (event) => event.preventDefault());
option.addEventListener("click", () => commit(item));
nodes.push(option);
row.addEventListener("pointerdown", event => { if (event.target !== star) event.preventDefault(); });
row.addEventListener("click", () => commit(item));
nodes.push(row);
optionIndex += 1;
}
}
if (!visibleItems.length) {
if (!filtered.length) {
const empty = document.createElement("div");
empty.className = "font-options-empty";
empty.textContent = "没有匹配的字体";
nodes.push(empty);
}
unregisterUiCopySubtree(listbox);
listbox.replaceChildren(...nodes);
activeIndex = visibleItems.findIndex((item) => item.id === selectedId);
if (activeIndex < 0 && visibleItems.length) activeIndex = 0;
setActive(activeIndex);
}
async function toggleFavorite(item, favorite) {
if (favoriteBusy || dialogOpen) return;
if (favorite) {
dialogOpen = true;
const dialog = document.createElement("dialog");
dialog.className = "font-favorite-dialog";
dialog.setAttribute("aria-label", "确认取消收藏");
const message = document.createElement("p");
renderUiCopy("copy.dynamic.font.confirm", message, "确认取消收藏字体“{name}/{style}”?", { name: item.family, style: item.style });
dialog.append(message);
for (const [value, title] of [["cancel", "取消"], ["confirm", "取消收藏"]]) {
const button = document.createElement("button");
button.type = "button";
renderUiCopy(value === "cancel" ? "copy.dynamic.font.cancel" : "copy.dynamic.font.remove", button, title);
button.addEventListener("click", () => dialog.close(value));
dialog.append(button);
}
document.body.append(dialog);
const result = new Promise(resolve => dialog.addEventListener("close", () => resolve(dialog.returnValue === "confirm"), { once: true }));
dialog.showModal();
const confirmed = await result;
unregisterUiCopySubtree(dialog);
dialog.remove();
dialogOpen = false;
if (!confirmed) { restoreInputFocus(); return; }
}
favoriteBusy = true;
render(queryText);
try {
const result = await onFavorite?.(item, !favorite);
if (Array.isArray(result)) favoriteIds = [...result];
} finally {
favoriteBusy = false;
render(queryText);
restoreInputFocus();
}
}
function restoreInputFocus() {
restoringFocus = true;
input.focus({ preventScroll: true });
restoringFocus = false;
}
function open(query = "") {
if (!enabled) return;
const wasClosed = listbox.hidden;
render(query);
listbox.hidden = false;
input.setAttribute("aria-expanded", "true");
if (wasClosed) onOpen?.();
}
input.addEventListener("focus", () => {
if (!enabled) return;
if (!enabled || restoringFocus) return;
input.select();
open();
});
@@ -179,7 +263,15 @@ export function createFontPicker({ input, listbox, onCommit }) {
}
});
input.addEventListener("blur", () => {
closeTimer = window.setTimeout(() => close(), 0);
closeTimer = window.setTimeout(() => {
if (!dialogOpen && !favoriteBusy && !listbox.contains(document.activeElement)) close();
}, 0);
});
listbox.addEventListener("focusout", () => {
closeTimer = window.setTimeout(() => {
if (!dialogOpen && !favoriteBusy && document.activeElement !== input && !listbox.contains(document.activeElement)) close();
}, 0);
});
restoreLabel();
@@ -187,7 +279,11 @@ export function createFontPicker({ input, listbox, onCommit }) {
setItems(nextItems) {
items = normalizeFontItems(nextItems);
restoreLabel();
if (!listbox.hidden) render();
if (!listbox.hidden) render(queryText);
},
setFavorites(ids) {
favoriteIds = Array.isArray(ids) ? [...ids] : [];
if (!listbox.hidden && !favoriteBusy && !dialogOpen) render(queryText);
},
setValue(identifier) {
selectedId = typeof identifier === "string" && identifier ? identifier : "default";
@@ -3158,3 +3158,17 @@ noscript {
#actual-screen-frame[hidden] { display:none; }
#actual-screen-frame span { position:absolute; top:2px; left:3px; font-size:11px; color:#FFCC66; background:#000b; padding:2px; white-space:nowrap; }
.board-expanded #actual-screen-frame { inset:33.333333%; }
.font-option-row { display:flex; align-items:center; gap:6px; border-radius:8px; padding-right:10px; }
.font-option-row .font-option { flex:1; min-width:0; }
.font-option-row small { flex:0 0 auto; max-width:30%; overflow:hidden; text-overflow:ellipsis; color:var(--muted); }
.font-option-row:has(.font-option.active), .font-option-row:hover { background:rgb(53 194 160 / 16%); }
.font-favorite-button { flex:0 0 44px; width:44px; min-height:44px; padding:0; background:transparent; border:0; font-size:24px; color:var(--muted); }
.font-favorite-button[aria-pressed="true"] { color:#ffd166; }
.font-favorite-button:focus-visible { outline:2px solid var(--accent); outline-offset:-2px; }
.font-favorite-dialog { background:var(--surface, #111923); color:var(--text); border:1px solid var(--muted); border-radius:12px; max-width:calc(100vw - 32px); }
.font-favorite-dialog p { overflow-wrap:anywhere; }
.font-favorite-dialog button { margin:4px; }
.font-favorite-dialog::backdrop { background:rgb(0 0 0 / 60%); }
.font-options { right:auto; box-sizing:border-box; }
@@ -234,46 +234,264 @@
"scope": "text",
"source": "views/text.js::font_catalog_failed"
},
"copy.dynamic.media.state_uploading": {"text": "上传中", "scope": "media-import", "source": "views/media-import.js::state_uploading"},
"copy.dynamic.media.state_analyzing": {"text": "分析中", "scope": "media-import", "source": "views/media-import.js::state_analyzing"},
"copy.dynamic.media.state_awaiting": {"text": "等待设置", "scope": "media-import", "source": "views/media-import.js::state_awaiting"},
"copy.dynamic.media.state_queued": {"text": "排队中", "scope": "media-import", "source": "views/media-import.js::state_queued"},
"copy.dynamic.media.state_converting": {"text": "转换中", "scope": "media-import", "source": "views/media-import.js::state_converting"},
"copy.dynamic.media.state_failed": {"text": "失败", "scope": "media-import", "source": "views/media-import.js::state_failed"},
"copy.dynamic.media.queue_position": {"text": "{state} · 队列第 {position} 位", "scope": "media-import", "source": "views/media-import.js::queue_position"},
"copy.dynamic.media.preview": {"text": "代表帧 {index}", "scope": "media-import", "source": "views/media-import.js::preview"},
"copy.dynamic.media.output_name": {"text": "保存名称", "scope": "media-import", "source": "views/media-import.js::output_name"},
"copy.dynamic.media.fit_mode": {"text": "1:1 适配", "scope": "media-import", "source": "views/media-import.js::fit_mode"},
"copy.dynamic.media.mode_crop": {"text": "自由取景", "scope": "media-import", "source": "views/media-import.js::mode_crop"},
"copy.dynamic.media.mode_contain": {"text": "完整包含", "scope": "media-import", "source": "views/media-import.js::mode_contain"},
"copy.dynamic.media.mode_stretch": {"text": "拉伸", "scope": "media-import", "source": "views/media-import.js::mode_stretch"},
"copy.dynamic.media.crop_zoom": {"text": "取景缩放", "scope": "media-import", "source": "views/media-import.js::crop_zoom"},
"copy.dynamic.media.transparency": {"text": "透明像素填充色", "scope": "media-import", "source": "views/media-import.js::transparency"},
"copy.dynamic.media.padding": {"text": "留白颜色", "scope": "media-import", "source": "views/media-import.js::padding"},
"copy.dynamic.media.convert": {"text": "保存设置并开始转换", "scope": "media-import", "source": "views/media-import.js::convert"},
"copy.dynamic.media.cancel_source": {"text": "取消并删除源文件", "scope": "media-import", "source": "views/media-import.js::cancel_source"},
"copy.dynamic.media.retry": {"text": "重试", "scope": "media-import", "source": "views/media-import.js::retry"},
"copy.dynamic.media.cancel": {"text": "取消并删除", "scope": "media-import", "source": "views/media-import.js::cancel"},
"copy.dynamic.media.deleted": {"text": "任务和源文件已删除。", "scope": "media-import", "source": "views/media-import.js::deleted"},
"copy.dynamic.media.delete_failed": {"text": "删除任务失败:{error}", "scope": "media-import", "source": "views/media-import.js::delete_failed"},
"copy.dynamic.media.queued": {"text": "任务已加入转换队列。", "scope": "media-import", "source": "views/media-import.js::queued"},
"copy.dynamic.media.name_conflict": {"text": "保存名称已存在,请更换名称。", "scope": "media-import", "source": "views/media-import.js::name_conflict"},
"copy.dynamic.media.convert_failed": {"text": "无法开始转换:{error}", "scope": "media-import", "source": "views/media-import.js::convert_failed"},
"copy.dynamic.media.retry_failed": {"text": "重试失败:{error}", "scope": "media-import", "source": "views/media-import.js::retry_failed"},
"copy.dynamic.media.completed": {"text": "“{name}”转换完成,已加入内容库。", "scope": "media-import", "source": "views/media-import.js::completed"},
"copy.dynamic.media.empty": {"text": "当前没有等待或转换中的任务。", "scope": "media-import", "source": "views/media-import.js::empty"},
"copy.dynamic.media.list_failed": {"text": "任务列表读取失败:{error}", "scope": "media-import", "source": "views/media-import.js::list_failed"},
"copy.dynamic.media.uploaded": {"text": "上传完成,设备正在分析媒体。", "scope": "media-import", "source": "views/media-import.js::uploaded"},
"copy.dynamic.media.upload_failed": {"text": "上传失败:{error}", "scope": "media-import", "source": "views/media-import.js::upload_failed"},
"copy.dynamic.current_display.loading": {"text": "正在读取当前画面…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_loading"},
"copy.dynamic.current_display.load_failed": {"text": "当前画面读取失败:{reason}", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_load_failed"},
"copy.dynamic.current_display.pause": {"text": "暂停", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_pause"},
"copy.dynamic.current_display.resume": {"text": "继续播放", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_resume"},
"copy.dynamic.current_display.pausing": {"text": "正在暂停…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_pausing"},
"copy.dynamic.current_display.resuming": {"text": "正在继续播放…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_resuming"},
"copy.dynamic.current_display.paused": {"text": "动图已暂停", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_paused"},
"copy.dynamic.current_display.resumed": {"text": "动图继续播放", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_resumed"},
"copy.dynamic.current_display.speed_changing": {"text": "正在切换到 {speed}×…", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_speed_changing"},
"copy.dynamic.current_display.speed_changed": {"text": "已切换到 {speed}×", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_speed_changed"},
"copy.dynamic.current_display.control_failed": {"text": "动图播放控制失败:{reason}", "scope": "dialog:current-display-dialog", "source": "views/device.js::current_display_control_failed"}
"copy.dynamic.media.state_uploading": {
"text": "上传中",
"scope": "media-import",
"source": "views/media-import.js::state_uploading"
},
"copy.dynamic.media.state_analyzing": {
"text": "分析中",
"scope": "media-import",
"source": "views/media-import.js::state_analyzing"
},
"copy.dynamic.media.state_awaiting": {
"text": "等待设置",
"scope": "media-import",
"source": "views/media-import.js::state_awaiting"
},
"copy.dynamic.media.state_queued": {
"text": "排队中",
"scope": "media-import",
"source": "views/media-import.js::state_queued"
},
"copy.dynamic.media.state_converting": {
"text": "转换中",
"scope": "media-import",
"source": "views/media-import.js::state_converting"
},
"copy.dynamic.media.state_failed": {
"text": "失败",
"scope": "media-import",
"source": "views/media-import.js::state_failed"
},
"copy.dynamic.media.queue_position": {
"text": "{state} · 队列第 {position} 位",
"scope": "media-import",
"source": "views/media-import.js::queue_position"
},
"copy.dynamic.media.preview": {
"text": "代表帧 {index}",
"scope": "media-import",
"source": "views/media-import.js::preview"
},
"copy.dynamic.media.output_name": {
"text": "保存名称",
"scope": "media-import",
"source": "views/media-import.js::output_name"
},
"copy.dynamic.media.fit_mode": {
"text": "1:1 适配",
"scope": "media-import",
"source": "views/media-import.js::fit_mode"
},
"copy.dynamic.media.mode_crop": {
"text": "自由取景",
"scope": "media-import",
"source": "views/media-import.js::mode_crop"
},
"copy.dynamic.media.mode_contain": {
"text": "完整包含",
"scope": "media-import",
"source": "views/media-import.js::mode_contain"
},
"copy.dynamic.media.mode_stretch": {
"text": "拉伸",
"scope": "media-import",
"source": "views/media-import.js::mode_stretch"
},
"copy.dynamic.media.crop_zoom": {
"text": "取景缩放",
"scope": "media-import",
"source": "views/media-import.js::crop_zoom"
},
"copy.dynamic.media.transparency": {
"text": "透明像素填充色",
"scope": "media-import",
"source": "views/media-import.js::transparency"
},
"copy.dynamic.media.padding": {
"text": "留白颜色",
"scope": "media-import",
"source": "views/media-import.js::padding"
},
"copy.dynamic.media.convert": {
"text": "保存设置并开始转换",
"scope": "media-import",
"source": "views/media-import.js::convert"
},
"copy.dynamic.media.cancel_source": {
"text": "取消并删除源文件",
"scope": "media-import",
"source": "views/media-import.js::cancel_source"
},
"copy.dynamic.media.retry": {
"text": "重试",
"scope": "media-import",
"source": "views/media-import.js::retry"
},
"copy.dynamic.media.cancel": {
"text": "取消并删除",
"scope": "media-import",
"source": "views/media-import.js::cancel"
},
"copy.dynamic.media.deleted": {
"text": "任务和源文件已删除。",
"scope": "media-import",
"source": "views/media-import.js::deleted"
},
"copy.dynamic.media.delete_failed": {
"text": "删除任务失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::delete_failed"
},
"copy.dynamic.media.queued": {
"text": "任务已加入转换队列。",
"scope": "media-import",
"source": "views/media-import.js::queued"
},
"copy.dynamic.media.name_conflict": {
"text": "保存名称已存在,请更换名称。",
"scope": "media-import",
"source": "views/media-import.js::name_conflict"
},
"copy.dynamic.media.convert_failed": {
"text": "无法开始转换:{error}",
"scope": "media-import",
"source": "views/media-import.js::convert_failed"
},
"copy.dynamic.media.retry_failed": {
"text": "重试失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::retry_failed"
},
"copy.dynamic.media.completed": {
"text": "“{name}”转换完成,已加入内容库。",
"scope": "media-import",
"source": "views/media-import.js::completed"
},
"copy.dynamic.media.empty": {
"text": "当前没有等待或转换中的任务。",
"scope": "media-import",
"source": "views/media-import.js::empty"
},
"copy.dynamic.media.list_failed": {
"text": "任务列表读取失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::list_failed"
},
"copy.dynamic.media.uploaded": {
"text": "上传完成,设备正在分析媒体。",
"scope": "media-import",
"source": "views/media-import.js::uploaded"
},
"copy.dynamic.media.upload_failed": {
"text": "上传失败:{error}",
"scope": "media-import",
"source": "views/media-import.js::upload_failed"
},
"copy.dynamic.current_display.loading": {
"text": "正在读取当前画面…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_loading"
},
"copy.dynamic.current_display.load_failed": {
"text": "当前画面读取失败:{reason}",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_load_failed"
},
"copy.dynamic.current_display.pause": {
"text": "暂停",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_pause"
},
"copy.dynamic.current_display.resume": {
"text": "继续播放",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_resume"
},
"copy.dynamic.current_display.pausing": {
"text": "正在暂停…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_pausing"
},
"copy.dynamic.current_display.resuming": {
"text": "正在继续播放…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_resuming"
},
"copy.dynamic.current_display.paused": {
"text": "动图已暂停",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_paused"
},
"copy.dynamic.current_display.resumed": {
"text": "动图继续播放",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_resumed"
},
"copy.dynamic.current_display.speed_changing": {
"text": "正在切换到 {speed}×…",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_speed_changing"
},
"copy.dynamic.current_display.speed_changed": {
"text": "已切换到 {speed}×",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_speed_changed"
},
"copy.dynamic.current_display.control_failed": {
"text": "动图播放控制失败:{reason}",
"scope": "dialog:current-display-dialog",
"source": "views/device.js::current_display_control_failed"
},
"copy.dynamic.font.favorites": {
"text": "收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.empty": {
"text": "暂无收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.no_matches": {
"text": "没有匹配的收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.confirm": {
"text": "确认取消收藏字体“{name}/{style}”?",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.cancel": {
"text": "取消",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.remove": {
"text": "取消收藏",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.saved": {
"text": "字体已收藏",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.removed": {
"text": "已取消收藏字体",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.read_error": {
"text": "收藏状态读取失败,请重新打开字体列表",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
},
"copy.dynamic.font.save_error": {
"text": "收藏保存失败:{error}",
"scope": "text",
"source": "font-picker.js / views/text.js::favorites"
}
}
@@ -5206,6 +5206,111 @@
"source": "views/device.js::voltage_ok",
"text": "屏幕输入电压 {volts} V"
},
"copy.dynamic.font.cancel": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "取消"
},
"copy.dynamic.font.confirm": {
"kind": "dynamic_template",
"placeholders": [
"name",
"style"
],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "确认取消收藏字体“{name}/{style}”?"
},
"copy.dynamic.font.empty": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "暂无收藏字体"
},
"copy.dynamic.font.favorites": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "收藏字体"
},
"copy.dynamic.font.no_matches": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "没有匹配的收藏字体"
},
"copy.dynamic.font.read_error": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "收藏状态读取失败,请重新打开字体列表"
},
"copy.dynamic.font.remove": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "取消收藏"
},
"copy.dynamic.font.removed": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "已取消收藏字体"
},
"copy.dynamic.font.save_error": {
"kind": "dynamic_template",
"placeholders": [
"error"
],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "收藏保存失败:{error}"
},
"copy.dynamic.font.saved": {
"kind": "dynamic_template",
"placeholders": [],
"render": {
"type": "dynamic_template"
},
"scope": "text",
"source": "font-picker.js / views/text.js::favorites",
"text": "字体已收藏"
},
"copy.dynamic.media.cancel": {
"kind": "dynamic_template",
"placeholders": [],
@@ -32,6 +32,7 @@ const fontOptions = document.getElementById("text-font-options");
const importFontButton = document.getElementById("import-font");
const fontFileInput = document.getElementById("font-file");
const fontFeedback = document.getElementById("font-feedback");
let favoriteEpoch = 0;
let initialized = false;
let suppressForm = false;
let renderedSelectionId = null;
@@ -43,6 +44,25 @@ let acceptedFontExtensions = [".ttf", ".otf", ".ttc", ".otc"];
const fontPicker = createFontPicker({
input: fontInput,
listbox: fontOptions,
onOpen() {
const epoch = ++favoriteEpoch;
apiJson("/api/fonts").then(catalog => { if (epoch === favoriteEpoch) fontPicker.setFavorites(catalog.favorite_ids); }).catch(() => {
setFontFeedbackTemplate("copy.dynamic.font.read_error", "收藏状态读取失败,请重新打开字体列表", {}, true);
});
},
async onFavorite(item, favorite) {
favoriteEpoch += 1;
try {
const result = await apiJson("/api/fonts/favorite", {
method: "PUT", body: JSON.stringify({ id: item.id, favorite }),
});
setFontFeedbackTemplate(favorite ? "copy.dynamic.font.saved" : "copy.dynamic.font.removed", favorite ? "字体已收藏" : "已取消收藏字体");
return result.favorite_ids;
} catch (error) {
setFontFeedbackTemplate("copy.dynamic.font.save_error", "收藏保存失败:{error}", { error: error.message }, true);
return undefined;
}
},
onCommit(item) {
const selected = getSelectedTextElement();
if (!selected) return;
@@ -220,6 +240,7 @@ subscribeScene(renderSelection);
async function loadFontCatalog() {
const catalog = await apiJson("/api/fonts");
fontPicker.setItems(catalog.items);
fontPicker.setFavorites(catalog.favorite_ids);
acceptedFontExtensions = Array.isArray(catalog.accepted_extensions)
? catalog.accepted_extensions.map((value) => String(value).toLowerCase())
: acceptedFontExtensions;
@@ -20,6 +20,7 @@ from PIL import Image
from app.display.colors import parse_color
from app.display.text_renderer import FontResolver, TextOptions, render_text_layer
from app.persistence import atomic_write_bytes, atomic_write_many_bytes
from app.fonts.render_cache import cache_stale, stamp_cache, STAMP_SUFFIX, versioned_url
logger = logging.getLogger(__name__)
@@ -341,7 +342,7 @@ class TemplateStore:
def _write_thumbnail(self, template_id: str, digest: str, scene: dict[str, Any]) -> Path:
path = self._thumbnail_path(template_id, digest)
if path.exists():
if path.exists() and not cache_stale(path, scene, self._font_resolver):
return path
content = render_scene_thumbnail(scene, font_resolver=self._font_resolver)
self._write_bytes_atomic(path, content)
@@ -353,6 +354,7 @@ class TemplateStore:
except Exception:
path.unlink(missing_ok=True)
raise
stamp_cache(path, scene, self._font_resolver)
return path
def _parse_record_document(
@@ -488,7 +490,7 @@ class TemplateStore:
for record in records:
expected.add(record["thumbnail"])
path = self.thumbnails_dir / record["thumbnail"]
if not path.exists():
if not path.exists() or cache_stale(path, record["scene"], self._font_resolver):
try:
self._write_thumbnail(record["id"], record["digest"], record["scene"])
except Exception:
@@ -500,6 +502,8 @@ class TemplateStore:
path.unlink(missing_ok=True)
elif THUMBNAIL_RE.fullmatch(path.name) and path.name not in expected:
path.unlink(missing_ok=True)
elif path.name.endswith(STAMP_SUFFIX) and THUMBNAIL_RE.fullmatch(path.name[:-len(STAMP_SUFFIX)]) and path.name[:-len(STAMP_SUFFIX)] not in expected:
path.unlink(missing_ok=True)
def _ensure_unique_name(self, name: str, exclude_id: str | None = None) -> None:
key = name.casefold()
@@ -518,7 +522,7 @@ class TemplateStore:
"updated_at": record["updated_at"],
"digest": record["digest"],
"revision": record_revision(record),
"thumbnail_url": f"/api/templates/{record['id']}/thumbnail?v={record['digest']}",
"thumbnail_url": versioned_url(f"/api/templates/{record['id']}/thumbnail?v={record['digest']}", record["scene"], self._font_resolver),
"size_bytes": size_bytes,
}
@@ -0,0 +1,15 @@
# 类似公交窄体(推荐32px)
独立16×32字格,末列留空。ID 为 `builtin:walnut-bus-16x32`,成品为 `app/fonts/bundled/SimilarBus16x32.ttf`。字符集与优化版16px一致(6763汉字、95 ASCII、26标点/符号),不增加其他语言承诺。32px是主要字号,64px为整数倍;不把32px款缩小到16px当成小字优化。
中文完整骨架来自 GNU Unifont 18.0.01 标准 G-source BDF,官方来源/版本/下载摘要见 `upstream/source.json`。选择 SIL OFL 1.1 制作衍生字体;保留原始版权、OFL及双许可证说明。字体未使用附件姚体轮廓。构建程序是项目自己的实现,没有复制Unifont的GPL构建代码。
设计不是将旧8×16放大。每个字从原16列中文骨架开始,横画与独立竖画主要2px,相邻部件留空,不够空间时保持1px。密集字的横画改单行,竖画保持连续;斜画重新补偏移阶梯;包围字重新绘制外框、内横、贯穿竖画。英文大写/数字从项目独立骨架重新安排26px字身及双列主笔画,小写单独分配字身、侧边距、基线和下伸部。
`glyphs.json` 保存所有32行、16位十六进制字模,`revisions.json` 保存覆盖与原因,`classes.json` 登记每类不少于8个代表字。method=grid-redraw为独立网格绘制,method=structure-rule为分类规则生成;后者不得称作逐字手工设计。算法在 `scripts/bus_font_design.py`,轮廓构建在 `scripts/build_bus_font.py`。共享字体构建锁及本地wheel位于相邻 `walnut-bus-8x16/`,不依赖仓库外文件。
从已准备的项目字体环境运行 `build_bus_font.py` 可离线构建两款;仅显式 `--design` 会重新生成分类字模。使用 `proof_bus_font.py --height 32 --output <受保护临时目录>` 生成全码位图册、密度/重复审计。输入摘要在SHA256SUMS;成品摘要和旧版淘汰/别名登记在bundle manifest。
推荐字号逐像素检查与字模一致、无灰阶、无越格;重点字64px与最近邻放大一致。分类词语和实际网页预览需额外视觉检查,不以覆盖或密度分数代替辨识判断。极密集罕用字仍可能需要上下文,未做真实用户辨识率实验。
更新字体后,受影响模板/动图的可再生缩略图重建,并写 `.font-version` 辅助缓存、增加 font_v URL参数;作品JSON、scene digest、修订号不变。辅助缓存只与登记缩略图共存,源图删除后按原资源清理规则删除对应辅助缓存。
@@ -0,0 +1,10 @@
c3717734b8165bfe80401ecfc002366fee6d9da687f3f6f90c2d3df75de4d1f4 classes.json
dcc861d8e2fc344d69e158ade174d0ebf14e19a6f623dfb71baf5451589fb270 glyphs.json
c346fc8ab3bc4f073c858fb24e90b94727a418db8cc6b2341356cc883e6072f1 README.md
0f72b70c49ec57f38fdeea4571203b3590a5105ea4bbde993e6e02e1bdefaae7 revisions.json
2b0a6c1018dc726790c621c853563a3a1e2d6e94f1f4499c2c74a2c34a20c148 upstream/COPYRIGHT.txt
1e74cb82bf476843e97c2596297b04219b1a7e51f7238944a8c031cb9401fa87 upstream/LICENSE.txt
869692af094c57fb7258c57fe26820c759319603321d0ffeb278de3651763ded upstream/OFL-1.1.txt
718dcc1ff27c21302c5d69cb5053776a945d5e1de3d8d1089cc97ab4f0d2a37f upstream/source.json
8c3c5ce5fb524eec5bacbc91b5ddfde81641e2cfc015521e95107b77570368e3 upstream/unifont-18.0.01.bdf
2989e0211030d8219fd931aba599fc95c86bc327ce1423848ef949783b208257 upstream/unifont-18.0.01.bdf.gz
@@ -0,0 +1,93 @@
{
"enclosure": [
"口",
"日",
"目",
"田",
"由",
"甲",
"申",
"回",
"国",
"园",
"圈",
"围"
],
"left_right": [
"站",
"铁",
"路",
"湖",
"树",
"谢",
"桥",
"城",
"明",
"林",
"河",
"村"
],
"stacked": [
"高",
"花",
"草",
"重",
"量",
"喜",
"音",
"意",
"章",
"营",
"售",
"票"
],
"diagonal": [
"人",
"入",
"大",
"文",
"交",
"永",
"木",
"水",
"火",
"公",
"交",
"父"
],
"dense": [
"鹤",
"警",
"藏",
"赢",
"鬻",
"醐",
"圜",
"鲴",
"醴",
"曲",
"瞒",
"圈"
],
"latin": [
"0",
"O",
"1",
"I",
"l",
"5",
"S",
"8",
"B",
"2",
"Z",
"6",
"G",
"9",
"g",
"M",
"W",
"m",
"w"
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
COPYRIGHT "Copyright (C) 1998-2026 Roman Czyborra, Paul Hardy, Qianqian Fang, Andrew Miller, Johnnie Weaver, David Corbett, Ælla Chiana Moskopp, Rebecca Bettencourt, Minseo Lee, Ho-Seok Ee, et al. License: SIL Open Font License version 1.1 and GPLv2+: GNU GPL version 2 or later <http://gnu.org/licenses/gpl.html> with the GNU Font Embedding Exception."
@@ -0,0 +1,476 @@
LICENSE
-------
The source code for everything except the compiled fonts in this current
release is licensed as follows:
License for this current distribution of program source
files (i.e., everything except the fonts) is released under
the terms of the GNU General Public License version 2,
or (at your option) a later version.
See the section below for a copy of the GNU General Public License
version 2.
The license for the compiled fonts is covered by the SIL Open Font License
version 1.1 and by the above GPL terms with the GNU font embedding exception,
as follows:
As a special exception, if you create a document which uses this font,
and embed this font or unaltered portions of this font into the document,
this font does not by itself cause the resulting document to be covered
by the GNU General Public License. This exception does not however
invalidate any other reasons why the document might be covered by the
GNU General Public License. If you modify this font, you may extend
this exception to your version of the font, but you are not obligated
to do so. If you do not wish to do so, delete this exception statement
from your version.
See "http://www.gnu.org/licenses/gpl-faq.html#FontException" and
"https://scripts.sil.org/OFL" for more details.
GPL VERSION 2
-------------
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
Also add information on how to contact you by electronic and paper mail.
If the program is interactive, make it output a short notice like this
when it starts in an interactive mode:
Gnomovision version 69, Copyright (C) year name of author
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, the commands you use may
be called something other than `show w' and `show c'; they could even be
mouse-clicks or menu items--whatever suits your program.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the program, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
`Gnomovision' (which makes passes at compilers) written by James Hacker.
<signature of Ty Coon>, 1 April 1989
Ty Coon, President of Vice
This General Public License does not permit incorporating your program into
proprietary programs. If your program is a subroutine library, you may
consider it more useful to permit linking proprietary applications with the
library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License.
SIL Open Font License version 1.1
---------------------------------
Copyright (c) <dates>, <Copyright Holder> (<URL|email>),
with Reserved Font Name <Reserved Font Name>.
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>),
with Reserved Font Name <additional Reserved Font Name>.
Copyright (c) <dates>, <additional Copyright Holder> (<URL|email>).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,90 @@
The SIL Open Font License version 1.1 is copied below, and is also
available with a FAQ at http://scripts.sil.org/OFL.
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,21 @@
{
"version": "18.0.01",
"license_choice": "OFL-1.1",
"files": [
{
"file": "unifont-18.0.01.bdf.gz",
"url": "https://unifoundry.com/pub/unifont/unifont-18.0.01/font-builds/unifont-18.0.01.bdf.gz",
"sha256": "2989e0211030d8219fd931aba599fc95c86bc327ce1423848ef949783b208257"
},
{
"file": "OFL-1.1.txt",
"url": "https://unifoundry.com/OFL-1.1.txt",
"sha256": "869692af094c57fb7258c57fe26820c759319603321d0ffeb278de3651763ded"
},
{
"file": "LICENSE.txt",
"url": "https://unifoundry.com/LICENSE.txt",
"sha256": "1e74cb82bf476843e97c2596297b04219b1a7e51f7238944a8c031cb9401fa87"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
# 类似公交窄体(推荐16px)
优化版字格固定 8×16,16px 为主要字号,中文最多占 7 列墨迹,末列留空。覆盖 GB2312 的 6763 个汉字、95 个可打印 ASCII 及 26 个中英文标点/符号,共 6884 个码位。32/48/64px 可按整数倍放大;需要32px中文时优先选择独立设计的16×32字款。其他字号允许使用但可能产生灰阶。保持现有渲染器,不替换自动多语言默认字体。
稳定选择 ID:`builtin:walnut-bus-8x16`。选择器与TTF名称:**类似公交窄体(推荐16px)**。成品为 `app/fonts/bundled/SimilarBus8x16.ttf`,按摘要安装到 `/var/lib/matrix-screen-controller/fonts/`。旧版内置 ID 和已知旧摘要引用转到优化版;校验并安装两款新字体后,仅删除摘要一致的旧公交字体,不保留旧版选项,不删除其他导入字体。旧作品结构、修订号和默认设置不变,但使用旧公交字体的作品显示优化字形。无需用户手动导入。
## 字模和来源
`glyphs.json` 是完整的 16 行十六进制字模;每行高位在左。`revisions.json` 保存逐字改绘和理由,覆盖基础字模。`upstream/source.json` 固定官方发布 URL、版本和原 ZIP SHA-256。基础来源为 Fusion Pixel Font 2026.09.25 简体中文 8px BDF;附件姚体只用于视觉比较,成品未使用其轮廓。
设计把横画保持为单像素,竖画连续延长,孤立斜画补阶梯连接;英文字母/数字及重点字逐字修订。所有共享像素边必须先抵消,再生成封闭轮廓,不能用相邻矩形直接拼接(FreeType 会产生 192-alpha 接缝)。TTF 左侧 bearing 必须等于轮廓 xMin,避免字符水平位移。构建时间固定,确保离线重复构建字节一致。
成品及改绘字模沿用 SIL OFL 1.1。原作者与上游许可完整保存在 `upstream/OFL.txt` 和 `upstream/LICENSES/`,运行资源内也附带同一套许可。独立字体名为 SimilarBus8x16 / 类似公交窄体(推荐16px)。
## 离线重建
字体构建/校样环境使用此目录的 `requirements-build.lock.txt` 和 `wheels/`,安装前核对 SHA-256。项目专用环境在 `测试相关资料/如何测试/本机测试环境/.local/font-build/`;无需系统安装字体,也不修改 PATH。
从仓库根执行,Python 路径用当前项目已搭建的环境:
Windows 首次准备专用环境(无需联网):
```powershell
python -m venv "测试相关资料/如何测试/本机测试环境/.local/font-build"
& "测试相关资料/如何测试/本机测试环境/.local/font-build/Scripts/python.exe" -m pip install --no-index --find-links "核桃派软件源代码/font_sources/walnut-bus-8x16/wheels" --require-hashes -r "核桃派软件源代码/font_sources/walnut-bus-8x16/requirements-build.lock.txt"
& "测试相关资料/如何测试/本机测试环境/.local/font-build/Scripts/python.exe" "核桃派软件源代码/scripts/build_bus_font.py"
```
已存在的环境直接复用,不重复创建;字体构建锁目前针对 CPython 3.14 / Windows x64。设备仅加载已经构建的 TTF,沿用原运行依赖。`SHA256SUMS` 保存该目录输入材料摘要,字体成品摘要位于运行资源 manifest。修改字模后同步重建成品及更新输入摘要。
```text
python 核桃派软件源代码/scripts/build_bus_font.py
python 核桃派软件源代码/scripts/proof_bus_font.py --output <受保护临时目录>/字体校样
```
`--seed` 从固定 Fusion BDF 重新生成8×16初始字模;`--design` 重新生成两款的分类修订和16×32骨架,均须显式调用,正常构建不改字模。人工进一步改绘应写入 revisions,不依赖一次性脚本。`--output` 指定隔离成品目录。校样使用 `--height 16` 或 `--height 32`,可加 `--reference <参考TTF>`;参考文件不参与构建或软件资源。`compare_bus_fonts.py --old <旧版TTF> --output <受保护临时目录>` 生成四栏对比及分类样张。
本轮额外重绘包围框及贯穿笔画,改善密集连接与稀疏斜画。分类算法见 `scripts/bus_font_design.py`,所有规则产出的逐字修订明确标记 method,不把规则生成记录冒充手工改绘。
## 质量边界
全部码位进行非空、精确字模、二值像素、字宽和边界检查,图册按码位分页复核。空格与全角空格共用空字形属于预期;其他目标码位不得产生重复像素字形。8 列空间的复杂汉字采用低分辨率简化,并非标准印刷字形;“鹤、警、藏、赢、鬻”等密集字应结合词语辨识,不能宣称每个罕用复杂字都与普通印刷字体同样易认。首版图册检查不等于用户辨识率实验。
测试选测涵盖字体、文字预览、场景、模板、REST;不涉及驱动、扫描、相机、ADC、网络切换或 Android 发布。一次性证据见仓库的对应测试归档。
@@ -0,0 +1,14 @@
6c4324e94df113077d3325e8bc37f366c54527058da1af102a7625157fdc5acd glyphs.json
d0c1f6602ae4ee9efb88562159b8c8f9ae2ce040180329e3d9df3546888f6cb8 README.md
3d8de0c053ea57ffa999a746ca5b779117a275c7320bc7aa9663fd4d28932639 requirements-build.lock.txt
7e5ee2ebd819d5e175203f1305dc37a75864ce887916efd4fa30242783e8f313 revisions.json
bd76834c43d6882184356394cfdd8c9c5e5623d9d1c9020564fe545b7ba769ce upstream/fusion-8px-bdf.zip
3c00ccab762c0c75967eea724f5cd57feb544be7a0814d06233160ef9482a50b upstream/fusion-pixel-8px-monospaced-zh_hans.bdf
ad588b5ce58a02179b806381339fd380e5110771f2fa91279af026fd3ab97002 upstream/LICENSES/boutique-bitmap-7x7/OFL.txt
86a3ee9495f942f0243f18c103da9faca27adb88142613edb8bb852e56c892c1 upstream/LICENSES/galmuri/LICENSE.txt
82929cc3b34c79b6a67f21fe137c7bb165589c9e34ba1441611e493afd67dfca upstream/LICENSES/misaki/misaki.txt
e855b45b384d37c9a778119271bd9c32706e3f1f9f278de0ad47347606eea846 upstream/LICENSES/miseki-bitmap/LICENSE.txt
bc518cf64b8032c07690f33cc270c35c179255a6ac8efa7c165ebae7e8f76a63 upstream/OFL.txt
adaeb53aa0cf63d7ad54baa2af1d686f3dea8986d4c393895bcd4d4adba9d64f upstream/source.json
7234ae9e28db64273fbbfa72caebd0a97e3bdba6b05064114741b9539ef339d0 wheels/fonttools-4.66.1-py3-none-any.whl
fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d wheels/pillow-12.3.0-cp314-cp314-win_amd64.whl
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,3 @@
# Font build/proof: CPython 3.14 Windows x64; official PyPI wheels.
fonttools==4.66.1 --hash=sha256:7234ae9e28db64273fbbfa72caebd0a97e3bdba6b05064114741b9539ef339d0
pillow==12.3.0 --hash=sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
[BoutiqueBitmap7x7]
2020-2026《字言字型》Cen-cyun, Liu. Luke Liu.
https://fontspeech.blogspot.com/
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
此字型是免費的。
無論您是否進行對本字型進行商業或非商業性修改,均可無限制地使用,複製和分發它們。
本字型的衍生品之授權必須與此字型相同,且不作任何擔保。
[MisakiGothic]
Copyright (C) 2002-2019 Num Kadoma
These fonts are free software.
Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.
THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
これらのフォントはフリー(自由な)ソフトウエアです。
あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
[观致]
这是日文字体Misaki基础上补充的
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
Copyright (c) 2019–2025 Lee Minseo (quiple@quiple.dev)
This font software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,258 @@
===============================================================================
8×8 ドット日本語フォント「美咲フォント」
(2021-05-05 版)
Copyright(C) 2002-2021 Num Kadoma
===============================================================================
-------------------------------------------------------------------------------
概要
-------------------------------------------------------------------------------
 美咲フォントは 8×8 ドットの日本語ビットマップフォントです。
 JIS第一・第二水準をサポートしています。
 一部の記号を除いた全ての文字は 7×7 ドットの範囲に収まっているため、
 文字同士を隣接させても行間・字間が確保できます。
 フォント名は制作のきっかけとなった美咲 礼威氏からいただきました。
-------------------------------------------------------------------------------
バリエーション
-------------------------------------------------------------------------------
・「美咲明朝」は、美咲ゴシックの非漢字部を明朝風の字形に差し替えたものです。
・「美咲ゴシック第2」は美咲ゴシックをベースに、半角文字は縦 7 px、
 全角仮名は横 7 px をいっぱいに使う字形に差し替えたものです。
 各機種用のアーカイブに、差替え済みのフォントファイルを同梱してあります。
-------------------------------------------------------------------------------
ライセンス
-------------------------------------------------------------------------------
 These fonts are free softwares.
 Unlimited permission is granted to use, copy, and distribute it, with or without modification, either commercially and noncommercially.
 THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY.
 これらのフォントはフリー(自由な)ソフトウエアです。
 あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、複製、再配布することができますが、全て無保証とさせていただきます。
-------------------------------------------------------------------------------
制作履歴
-------------------------------------------------------------------------------
■2021-05-05
・文字を追加
 共通: ␣
・字形を修正・変更
 共通: №
 ゴシック第2: プヴ
・(TTF) ビットマップを埋込み
■2019-10-19
・ドキュメントの文字コードを UTF-8N、改行コードを LF に統一
・(TTF, BDF) 記号を追加:
  ▁▂▃▄▅▆▇█▏▎▍▌▋▊▉▔▕╭╮╰╯♠♥♦♣╱╲╳♤♡♢♧
■2019-08-18
・各ファイルのパーミッションから、不要な実行ビットを除去 (フォントの変更はなし)
■2019-06-03
・新フォント「美咲ゴシック第2」を追加。英語名「MisakiGothic2nd」
・「令和」の合字を追加
・各種字形の修正
・(E500) ファイル名のつけ方を変更して配布を再開
・(BDF) 一部仕様を変更して配布を再開
  エンコーディングは ISO 10646-1 のみとし、全ての文字を 1 ファイルに収録
  XLFD 変更
  BITMAP を文字の上下左右端の空白を削った形とし、BBX で位置を指定する形に修正
■2019-02-03a
・制作履歴における、変更した文字の抜け (ゴシックの「と」「ね」「ゐ」) を修正
■2019-02-03
・ゴシック・明朝共通の変更: &,.‘’()〔〕[]{}〈〉【】°′″№0索二
  半角 (「&」字形変更)
  記号類 (主に括弧類の位置調整)
  数字 (ゼロにスラッシュを追加)
  漢字 (「索」「二」の字形変更)
・ゴシックのみの変更: 69CGOQΟОСЭぐぜとどねばぶゐガグゾダバヴ
  数字 (「6」「9」の先端を伸ばす形に変更)
  アルファベット (オーなどの丸み変更)
  仮名 (主に濁点/半濁点つき文字の調整)
・明朝のみの変更: ぐでとどなぶウグケサザソゾダドワヱヴЁ
  仮名 (各種字形変更)
  キリル (「Ё」の字形変更)
■2015-04-10
・誤字になっていた「喪」を修正
・(TTF) ウェイトを「Regular」に変更
  一部の記号を追加、変更
  バージョン番号を「yyyy.mmdd」の形に変更
  その他、パラメータを見直し
  埋込みビットマップ版の同梱を取り止め
■2012-06-03
・正式公開初版
■2012-03-31
・各種ファイル名、ドキュメントの見直し
・BDF 版以外のアーカイブ形式を zip に変更
・(TTF) アウトライン版の正式採用、Mac におそらく対応
・(FONTX) 4X8.FNT の 00h~1Fh に DOS/V 罫線を追加
■2011-03-08
・(TTF-Outline) 生成手順の改善に伴い periodβ11 作成・公開
■2010-03-07
・(BDF) FOUNDRY, POINT_SIZE, AVERAGE_WIDTH, SWIDTH, XLFD, 各ファイル名の変更
■2006-10-01
・(TTF-Outline) 試作版配布開始
■2006-04-01
・(PNG) 配布開始
■2006-03-26
・(E500) 13 区追加版・軽量版 (第一水準のみ) 同梱開始
  フォントファイルの命名規則変更
・(BDF) 美咲明朝の配布形態をゴシックとの差分から実ファイル同梱へ
  BITMAP の記述方法を修正 (16 進 4 桁から 2 桁へ)
・(FONTX) 配布再開
■2005-07-20
・アーカイブの命名規則を変更
・(TTF, BDF) 著作権表記を変更
・(BDF) 1 バイト文字フォントのファイル名をそれぞれ 4x8.bdf, 4x8_8859.bdf に
■2004-09-12
・(TTF, BDF) 13 区の文字追加
■2004-06-24
・フォントの名称変更
・(美咲フォントを美咲ゴシック、ファミリーの総称を美咲フォントに)
■2004-05-09
・美咲明朝の同梱開始
■2004-04-19
・半濁点の処理を変更
■2004-04-11
・3 フォント同時編集ツール「"SCRNJPN.FNT" Editor」完成
■2004-02-09
・濁点の処理を変更
■2003-09-21
・制作履歴の日付ミスを修正 (美咲氏 Thanx!)
■2003-06-04
・ライセンスに関する記述を変更
■2003-05-04
・(BDF) 3x7_8859.bdf の CHARS の値を修正
・(BDF) 3x7_8859.bdf のファイル名が 3X7_8859.bdf になっていたのを修正
■2003-03-26
・マニュアルをフォントの説明と各形式の説明に分離
■2003-01-02
・各部のフォント間での統一化作業を本格的に開始
■2002-06-10
・JIS X 0208-1983 での文字の変更、JIS X 0208-1990 での文字の追加に対応
■2002-06-06
・未定義文字のフォントを 3×3 の ■ に
■2002-06-01
・第二水準完成
■2002-05-22
・制作再開
■2002-05-21
・フォント作成専用ツール「'SCRNJPN.FNT' Editor」完成
■2002-05-11
・E650 暴走により第二水準中の 600 文字近くが消滅→制作中断
■2002-04-初
・第一水準完成
■2002-03-中
・制作開始
-------------------------------------------------------------------------------
バージョン情報
-------------------------------------------------------------------------------
■2021-05-05
・美咲フォント 2021-05-05 版 (E500/BDF/TTF/PNG)
■2019-10-19
・美咲フォント 2019-10-19 版 (E500/BDF/TTF/PNG)
■2019-08-18
・美咲フォント 2019-06-03a 版 (E500/BDF/TTF/PNG)
■2019-06-03
・美咲フォント 2019-06-03 版 (E500/BDF/TTF/PNG)
■2019-02-03
・美咲フォント 2019-02-03 版 (TTF/PNG)
■2015-04-10
・美咲フォント 2015-04-10 版 (TTF/PNG)
■2012-06-03
・美咲フォント 2012-06-03 版 (E500/BDF/TTF/Ruputer/FONTX/PNG)
■2012-03-31
・美咲フォント periodβ12 (E500/BDF/TTF/Ruputer/FONTX/PNG)
■2010-03-07
・美咲フォント periodβ11a (BDF)
■2008-06-03
・美咲フォント periodβ11 (E500/BDF/TTF/Ruputer/FONTX/PNG/TTF-Outline)
■2006-03-26
・美咲フォント periodβ10 (E500/BDF/TTF/Ruputer/FONTX/PNG/TTF-Outline)
■2005-07-20
・美咲フォント periodβ9 (E500/BDF/TTF/Ruputer)
■2004-05-09
・美咲フォント periodβ8 (E500/BDF/TTF/Ruputer)
■2003-09-21
・美咲フォント periodβ7 (E500/BDF/TTF/Ruputer)
■2003-06-04
・美咲フォント periodβ6 (E500/BDF/TTF/Ruputer)
■2003-05-04
・美咲フォント periodβ5 (E500/BDF/TTF/Ruputer)
■2003-03-26
・美咲フォント periodβ4 (E500/BDF/TTF/Ruputer/FONTX)
■2002-12-14
・美咲フォント periodβ3 (E500)
■2002-06-18
・美咲フォント periodβ2 (E500)
■2002-06-03
・美咲フォント periodβ1 (E500)
-------------------------------------------------------------------------------
連絡先
-------------------------------------------------------------------------------
 門真 なむ (Num Kadoma)
 ・Twitter: @num_kadoma
 ・Website: http://littlelimit.net/
@@ -0,0 +1,92 @@
Copyright (c) 2023-2024 Mark Li (itmarkibfb@gmail.com)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,96 @@
Fusion Pixel Font
https://github.com/TakWolf/fusion-pixel-font
Copyright (c) 2022, TakWolf (https://takwolf.com).
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bd76834c43d6882184356394cfdd8c9c5e5623d9d1c9020564fe545b7ba769ce
size 21668470
@@ -0,0 +1,5 @@
{
"url": "https://github.com/TakWolf/fusion-pixel-font/releases/download/2026.09.25/fusion-pixel-font-8px-monospaced-bdf-v2026.09.25.zip",
"version": "2026.09.25",
"sha256": "bd76834c43d6882184356394cfdd8c9c5e5623d9d1c9020564fe545b7ba769ce"
}
@@ -0,0 +1,2 @@
7234ae9e28db64273fbbfa72caebd0a97e3bdba6b05064114741b9539ef339d0 fonttools-4.66.1-py3-none-any.whl
fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d pillow-12.3.0-cp314-cp314-win_amd64.whl
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7234ae9e28db64273fbbfa72caebd0a97e3bdba6b05064114741b9539ef339d0
size 1202222
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d
size 7237707
@@ -0,0 +1,162 @@
"""Offline build of two independent bus fonts from Unicode bitmap masters."""
from __future__ import annotations
import argparse
import hashlib
import json
from pathlib import Path
from fontTools.fontBuilder import FontBuilder
from fontTools.pens.ttGlyphPen import TTGlyphPen
from bus_font_design import parse_bdf, generate_large, improve_small
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "font_sources/walnut-bus-8x16"
OUTPUT = ROOT / "app/fonts/bundled"
LARGE_SOURCE = ROOT / "font_sources/walnut-bus-16x32"
OLD_DIGEST = "d38bf29d6408a6f731865a1ca760a80b5156ebf6ab96d5ad8d649df28163564c"
def gb2312_codepoints() -> set[int]:
result = set(range(32, 127))
for first in range(0xA1, 0xF8):
for second in range(0xA1, 0xFF):
try:
cp = ord(bytes((first, second)).decode("gb2312"))
if 0x4E00 <= cp <= 0x9FFF:
result.add(cp)
except UnicodeDecodeError:
pass
result.update(map(ord, " ,。!?;:、‘’“”()《》【】…—¥℃→←↑↓"))
return result
def elongate(rows: list[int]) -> list[int]:
"""Keep horizontal stems thin; extend vertical/diagonal stems between rows."""
output = [0] * 16
for y, row in enumerate(rows):
output[2 * y] = row
if y < 7:
following = rows[y + 1]
isolated = row & ~((row << 1) | (row >> 1))
output[2 * y + 1] = (row & following) | (isolated & ((following << 1) | (following >> 1)))
return output
def seed() -> None:
upstream = SOURCE / "upstream/fusion-pixel-8px-monospaced-zh_hans.bdf"
original = parse_bdf(upstream)
required = gb2312_codepoints()
missing = sorted(required - original.keys())
if missing:
raise ValueError(f"upstream missing {len(missing)} target codepoints: {missing[:12]}")
glyphs = {f"{cp:04X}": [f"{row:02X}" for row in elongate(original[cp])] for cp in sorted(required)}
(SOURCE / "glyphs.json").write_text(json.dumps(glyphs, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def build_face(source: Path, width: int, height: int, output: Path) -> dict:
masters = json.loads((source / "glyphs.json").read_text(encoding="utf-8"))
overrides = json.loads((source / "revisions.json").read_text(encoding="utf-8"))
for key, revision in overrides.items():
masters[key] = revision["rows"]
required = gb2312_codepoints()
if not required <= {int(key, 16) for key in masters}:
raise ValueError("incomplete GB2312/ASCII masters")
builder = FontBuilder(1024, isTTF=True)
order = [".notdef"] + [f"uni{key}" for key in sorted(masters)]
builder.setupGlyphOrder(order)
glyphs = {}
for name in order:
pen = TTGlyphPen(None)
rows = [0] * height if name == ".notdef" else [int(row, 16) for row in masters[name[3:]]]
if len(rows) != height or any(row < 0 or row >= 1 << width for row in rows):
raise ValueError(f"invalid bitmap {name}")
# Cancel shared cell edges, then trace the union boundary. Separate
# touching rectangles produce 192-alpha seams in FreeType even on-grid.
edges = set()
for y, row in enumerate(rows):
for x in range(width):
if not row & (1 << (width - 1 - x)):
continue
step = 1024 // height
left, right = x * step, (x + 1) * step
bottom, top = (height - 1 - y) * step, (height - y) * step
points = [(left, bottom), (left, top), (right, top), (right, bottom)]
for a, b in zip(points, points[1:] + points[:1]):
if (b, a) in edges:
edges.remove((b, a))
else:
edges.add((a, b))
outgoing = {}
for a, b in edges:
outgoing.setdefault(a, set()).add(b)
while edges:
a, b = min(edges)
edges.remove((a, b))
outgoing[a].remove(b)
start = a
pen.moveTo(a)
while b != start:
pen.lineTo(b)
candidates = [(b, end) for end in sorted(outgoing[b])]
if not candidates:
raise ValueError(f"open outline {name}")
dx, dy = b[0] - a[0], b[1] - a[1]
def turn(edge):
nx, ny = edge[1][0] - b[0], edge[1][1] - b[1]
cross = dx * ny - dy * nx
return 0 if cross < 0 else 1 if dx * nx + dy * ny > 0 else 2
a, b = min(candidates, key=turn)
edges.remove((a, b))
outgoing[a].remove(b)
pen.closePath()
glyphs[name] = pen.glyph()
builder.setupGlyf(glyphs)
builder.setupHorizontalMetrics({name: (512, getattr(glyphs[name], "xMin", 0)) for name in order})
builder.setupHorizontalHeader(ascent=1024, descent=0, lineGap=0)
builder.setupCharacterMap({int(key, 16): f"uni{key}" for key in masters})
builder.setupNameTable({
"familyName": f"类似公交窄体(推荐{height}px)", "styleName": "Regular",
"uniqueFontIdentifier": f"SimilarBus{width}x{height}-2.0", "fullName": f"类似公交窄体(推荐{height}px)",
"psName": f"SimilarBus{width}x{height}-Regular", "version": "Version 2.000",
"copyright": ("Derived from GNU Unifont 18.0.01: Copyright (C) 1998-2026 Roman Czyborra, Paul Hardy, Qianqian Fang, Andrew Miller, Johnnie Weaver, David Corbett, Aella Chiana Moskopp, Rebecca Bettencourt, Minseo Lee, Ho-Seok Ee, et al.; Latin skeletons derived from project bus font."
if height == 32 else "Derived from Fusion Pixel Font; see accompanying OFL and upstream licenses."),
"licenseDescription": "SIL Open Font License, Version 1.1",
"licenseInfoURL": "https://openfontlicense.org/",
})
builder.setupOS2(sTypoAscender=1024, sTypoDescender=0, sTypoLineGap=0,
usWinAscent=1024, usWinDescent=0, fsType=0)
builder.setupPost()
builder.setupMaxp()
builder.font.recalcTimestamp = False
builder.font["head"].created = builder.font["head"].modified = 3864153600
output.mkdir(parents=True, exist_ok=True)
target = output / f"SimilarBus{width}x{height}.ttf"
builder.save(target)
digest = hashlib.sha256(target.read_bytes()).hexdigest()
manifest = {"id": f"builtin:walnut-bus-{width}x{height}", "file": target.name, "sha256": digest,
"label": f"类似公交窄体(推荐{height}px)", "family": f"类似公交窄体(推荐{height}px)", "style": "Regular",
"glyph_count": len(masters), "recommended_size": height, "cell": [width, height]}
return manifest
def build(output: Path = OUTPUT) -> dict:
fonts = [build_face(SOURCE, 8, 16, output), build_face(LARGE_SOURCE, 16, 32, output)]
manifest = {"version": 2, "fonts": fonts,
"obsolete": [OLD_DIGEST], "aliases": {f"user:{OLD_DIGEST}:0": fonts[0]["id"]}}
(output / "manifest.json").write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return manifest
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--seed", action="store_true", help="regenerate initial masters (discards master edits)")
parser.add_argument("--design", action="store_true", help="generate structure-specific masters/overrides for both faces")
parser.add_argument("--output", type=Path, default=OUTPUT)
args = parser.parse_args()
if args.seed:
seed()
if args.design:
improve_small(SOURCE)
generate_large(LARGE_SOURCE, SOURCE, gb2312_codepoints())
print(json.dumps(build(args.output), ensure_ascii=False))
@@ -0,0 +1,222 @@
"""Structure-specific grid design; no runtime rasterizer changes."""
from __future__ import annotations
import json
from pathlib import Path
CLASSES = {
"enclosure": "口日目田由甲申回国园圈围",
"left_right": "站铁路湖树谢桥城明林河村",
"stacked": "高花草重量喜音意章营售票",
"diagonal": "人入大文交永木水火公交父",
"dense": "鹤警藏赢鬻醐圜鲴醴曲瞒圈",
"latin": "0O1Il5S8B2Z6G9gMWmw",
}
def parse_bdf(path: Path, width: int = 8, height: int = 8, ascent: int = 7) -> dict[int, list[int]]:
result = {}
codepoint = 0
bounds = (0, 0, 0, 0)
bitmap = False
rows = []
for line in path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("ENCODING "):
codepoint = int(line.split()[1])
elif line.startswith("BBX "):
bounds = tuple(map(int, line.split()[1:]))
elif line == "BITMAP":
rows, bitmap = [], True
elif line == "ENDCHAR":
bw, bh, bx, by = bounds
grid = [0] * height
top = ascent - bh - by
for y, row in enumerate(rows):
padding = ((bw + 7) // 8) * 8 - width + bx
value = row >> padding if padding >= 0 else row << -padding
if not 0 <= top + y < height or value >= 1 << width:
raise ValueError(f"BDF outside {width}x{height}: U+{codepoint:04X}")
grid[top + y] = value
result[codepoint] = grid
bitmap = False
elif bitmap:
rows.append(int(line, 16))
return result
def pixel_rows(grid):
return [sum((1 << (len(row) - 1 - x)) for x, bit in enumerate(row) if bit) for row in grid]
def grid_rows(rows, width):
return [[bool(row & (1 << (width - 1 - x))) for x in range(width)] for row in rows]
def design_large(rows, *, dense=False, diagonal=False):
native = grid_rows(rows, 16)
out = [[False] * 16 for _ in range(32)]
for y in range(16):
for x in range(15):
if not native[y][x]:
continue
out[2 * y][x] = True
vertical = (y > 0 and native[y - 1][x]) or (y < 15 and native[y + 1][x])
# Dense horizontal bars are one row tall, with vertical stems
# retained on the intervening row. Other strokes use two rows.
out[2 * y + 1][x] = not dense or vertical or sum(native[y]) < 7
isolated = (x == 0 or not native[y][x - 1]) and not native[y][x + 1]
if vertical and isolated:
if x < 14 and not native[y][x + 1] and (x == 13 or not native[y][x + 2]):
out[2 * y][x + 1] = True
out[2 * y + 1][x + 1] = True
elif x > 1 and not native[y][x - 1] and not native[y][x - 2]:
out[2 * y][x - 1] = True
out[2 * y + 1][x - 1] = True
if diagonal and y < 15:
# Use an offset step on the intermediate row rather than
# a uniform two-row stair; the crossing point stays narrow.
for dx in (-1, 1):
nx = x + dx
if 0 <= nx < 15 and native[y + 1][nx] and not native[y + 1][x]:
out[2 * y + 1][nx] = True
return pixel_rows(out)
def enclosure(char, width, height):
grid = [[False] * width for _ in range(height)]
thick = 2 if height == 32 else 1
left, right = (1, 13) if height == 32 else (0, 6)
top, bottom = (3, 28) if height == 32 else (1, 14)
if height == 16 and char in "田由甲申":
top, bottom = 2, 12 # leave room for the distinguishing top/bottom stems
def box(x1, y1, x2, y2):
for y in range(y1, y2 + 1):
for x in range(x1, x2 + 1):
grid[y][x] = True
box(left, top, right, top + thick - 1)
box(left, bottom - thick + 1, right, bottom)
box(left, top, left + thick - 1, bottom)
box(right - thick + 1, top, right, bottom)
bars = {"日": [.5], "目": [.33, .67], "田": [.5], "由": [.5], "甲": [.5], "申": [.5]}
for fraction in bars.get(char, []):
y = round(top + (bottom - top) * fraction)
box(left + thick, y, right - thick, y + thick - 1)
if char in "田由甲申":
middle = width // 2 - 1
box(middle, 0 if char in "由申" else top + thick, middle + thick - 1,
height - 2 if char in "甲申" else bottom - thick)
if char == "回":
inset = 4 if height == 32 else 2
box(left + inset, top + inset, right - inset, top + inset + thick - 1)
box(left + inset, bottom - inset - thick + 1, right - inset, bottom - inset)
box(left + inset, top + inset, left + inset + thick - 1, bottom - inset)
box(right - inset - thick + 1, top + inset, right - inset, bottom - inset)
return pixel_rows(grid)
def latin_large(rows):
# Deliberately separate Latin design: 5-column skeleton expanded to 10
# columns, horizontal stems 2 rows, vertical stems fill the pitch gaps.
source = grid_rows(rows, 8)
skeleton = [source[y][1:6] for y in range(0, 14, 2)]
grid = [[False] * 16 for _ in range(32)]
for y, row in enumerate(skeleton):
yy = 2 + y * 4
for x, bit in enumerate(row):
if not bit:
continue
xx = 3 + x * 2
for dy in (0, 1):
for dx in (0, 1):
grid[yy + dy][xx + dx] = True
if y < 6 and skeleton[y + 1][x]:
for dy in (2, 3):
for dx in (0, 1):
grid[yy + dy][xx + dx] = True
elif y < 6:
for nx in (x - 1, x + 1):
if 0 <= nx < 5 and skeleton[y + 1][nx]:
for dy in (2, 3):
grid[yy + dy][xx + (1 if nx > x else 0)] = True
return pixel_rows(grid)
def latin_lower(rows):
native = grid_rows(rows, 16)
grid = [[False] * 16 for _ in range(32)]
for y in range(16):
for x in range(8):
if native[y][x]:
# Reserve a common side bearing and use the ten-column Latin
# body rather than leaving lowercase stranded at the left.
left = 3 + x * 10 // 8
right = 3 + (x + 1) * 10 // 8
for xx in range(left, right):
grid[2 * y][xx] = grid[2 * y + 1][xx] = True
return pixel_rows(grid)
def generate_large(source_dir: Path, small_dir: Path, required: set[int]):
native = parse_bdf(source_dir / "upstream/unifont-18.0.01.bdf", 16, 16, 14)
missing = required - native.keys()
if missing:
raise ValueError(f"Unifont missing target codepoints: {sorted(missing)}")
masters = {}
revisions = {}
for cp in sorted(required):
rows = design_large(native[cp], dense=sum(row.bit_count() for row in native[cp]) > 115)
masters[f"{cp:04X}"] = [f"{row:04X}" for row in rows]
for kind, characters in CLASSES.items():
if kind == "latin":
continue
for char in characters:
key = f"{ord(char):04X}"
if ord(char) not in required:
continue
rows = (enclosure(char, 16, 32) if char in "口日目田由甲申回" else
design_large(native[ord(char)], dense=kind == "dense", diagonal=kind == "diagonal"))
revisions[key] = {"rows": [f"{row:04X}" for row in rows], "class": kind,
"method": "grid-redraw" if char in "口日目田由甲申回" else "structure-rule",
"reason": {"enclosure": "重排包围框及内部留白。", "left_right": "分别增粗独立竖画,保留部件间空列。",
"stacked": "横画保留双行厚度,竖画避开相邻笔画。", "diagonal": "重排斜画阶梯及交叉点。",
"dense": "密集横画改单行,保留竖画连续及部件空隙。"}[kind]}
small_revisions = json.loads((small_dir / "revisions.json").read_text(encoding="utf-8"))
for key, revision in small_revisions.items():
cp = int(key, 16)
if 48 <= cp <= 57 or 65 <= cp <= 90 or chr(cp) in "il":
rows = latin_large([int(row, 16) for row in revision["rows"]])
revisions[key] = {"rows": [f"{row:04X}" for row in rows], "class": "latin", "method": "grid-redraw",
"reason": "独立重排拉丁骨架,双列主笔画及26px字身,区分易混字符。"}
for cp in range(ord("a"), ord("z") + 1):
if chr(cp) not in "il":
revisions[f"{cp:04X}"] = {"rows": [f"{row:04X}" for row in latin_lower(native[cp])],
"class": "latin", "method": "structure-rule",
"reason": "小写独立分配十列字身及侧边距,保留基线和下伸部。"}
for filename, data in [("glyphs.json", masters), ("revisions.json", revisions),
("classes.json", {kind: list(chars) for kind, chars in CLASSES.items()})]:
(source_dir / filename).write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
def improve_small(source_dir: Path):
revisions = json.loads((source_dir / "revisions.json").read_text(encoding="utf-8"))
masters = json.loads((source_dir / "glyphs.json").read_text(encoding="utf-8"))
for kind in ("dense", "diagonal"):
for char in CLASSES[kind]:
key = f"{ord(char):04X}"
rows = [int(row, 16) for row in masters[key]]
for y in range(1, 15, 2):
previous, following = rows[y - 1], rows[y + 1]
if kind == "dense":
common = previous & following
interior = common & (common << 1) & (common >> 1)
rows[y] &= ~interior
elif previous.bit_count() <= 4 and following.bit_count() <= 4:
rows[y] |= previous & ((following << 1) | (following >> 1))
revisions[key] = {"rows": [f"{row:02X}" for row in rows], "class": kind, "method": "structure-rule",
"reason": "密集处减少连续实心块,保留原横画与边缘竖画。" if kind == "dense" else "对稀疏撇捺补独立阶梯连接,保留交叉辨识。"}
for char in "口日目田由甲申回":
revisions[f"{ord(char):04X}"] = {"rows": [f"{row:02X}" for row in enclosure(char, 8, 16)],
"class": "enclosure", "method": "grid-redraw",
"reason": "使用16行重新安排包围框、内横画及贯穿竖画,避免仅扩展8行骨架。"}
(source_dir / "revisions.json").write_text(json.dumps(revisions, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
@@ -0,0 +1,79 @@
// Caller supplies an isolated browser context. No display/apply operation is performed.
import assert from "node:assert/strict";
export async function checkFontFavoritesBrowser(page, baseUrl, fontId = "default") {
const errors = [];
const onError = error => errors.push(error.message);
page.on("pageerror", onError);
const catalog = await (await page.request.get(`${baseUrl}/api/fonts`)).json();
const originallyFavorite = catalog.favorite_ids.includes(fontId);
const item = catalog.items.find(item => item.id === fontId);
assert.ok(item);
const update = favorite => page.request.put(`${baseUrl}/api/fonts/favorite`, { data: { id: fontId, favorite } });
const rows = () => page.locator(".font-option-row").filter({ has: page.locator(`[data-font-id="${fontId}"]`) });
const draft = () => page.evaluate(async () => JSON.stringify((await import("/static/scene-board.js")).getScene()));
try {
await update(false);
await page.goto(`${baseUrl}/#canvas`);
await page.locator("#new-text-layer").click();
await page.locator("#create-text").click();
const input = page.locator("#text-font");
await input.click();
assert.equal(await page.locator(".font-option-group").first().textContent(), "收藏字体");
const before = await draft();
const selected = await input.inputValue();
await rows().locator("button").first().click();
await page.waitForFunction(id => document.querySelectorAll(`[data-font-id="${id}"]`).length === 2, fontId);
assert.equal(await rows().locator('[aria-pressed="true"]').count(), 2);
assert.equal(await draft(), before);
assert.equal(await input.inputValue(), selected);
const ids = await page.locator('[role="option"]').evaluateAll(options => options.map(option => option.id));
assert.equal(new Set(ids).size, ids.length);
await rows().locator("button").first().click();
await page.locator(".font-favorite-dialog button").first().click();
assert.equal(await rows().count(), 2);
await rows().locator("button").first().click();
await page.keyboard.press("Escape");
assert.equal(await rows().count(), 2);
await input.fill(item.family);
assert.equal(await rows().count(), 2);
await input.press("Home");
await input.press("ArrowDown");
const active = await input.getAttribute("aria-activedescendant");
assert.equal(await page.locator(`[id="${active}"]`).count(), 1);
await input.press("End");
await input.press("Escape");
await input.click();
await input.press("Tab");
assert.equal(await page.locator(".font-favorite-button:focus").count(), 1);
await page.keyboard.press("Enter");
await page.locator(".font-favorite-dialog button").last().click();
await page.waitForFunction(id => document.querySelectorAll(`[data-font-id="${id}"]`).length === 1, fontId);
assert.equal(await draft(), before);
// A failed save keeps both catalog and UI unchanged.
await page.route("**/api/fonts/favorite", route => route.fulfill({ status: 500, contentType: "application/json", body: '{"detail":"injected failure"}' }));
await rows().locator("button").click();
await page.locator("#font-feedback[data-state=error]").waitFor();
assert.equal(await rows().count(), 1);
assert.equal(await rows().locator('[aria-pressed="false"]').count(), 1);
await page.unroute("**/api/fonts/favorite");
// Another browser changes the shared record; reopening reads it.
await input.press("Escape");
const other = await page.context().browser().newContext();
try {
const response = await other.request.put(`${baseUrl}/api/fonts/favorite`, { data: { id: fontId, favorite: true } });
assert.equal(response.status(), 200);
} finally { await other.close(); }
await input.click();
await page.waitForFunction(id => document.querySelectorAll(`[data-font-id="${id}"]`).length === 2, fontId);
assert.equal(await draft(), before);
const bounds = await page.locator("#text-font-options").boundingBox();
assert.ok(bounds.x >= 0 && bounds.x + bounds.width <= page.viewportSize().width + 1);
assert.deepEqual(errors, []);
return { passed: true, width: page.viewportSize().width, duplicateOptions: true, sceneUnchanged: true, errors };
} finally {
await page.unroute("**/api/fonts/favorite");
await update(originallyFavorite);
page.off("pageerror", onError);
}
}
@@ -0,0 +1,45 @@
"""Compare actual layer output, including old 16px enlarged and new 32px."""
from pathlib import Path
import argparse
import sys
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.fonts.store import FontCatalog
from app.display.text_renderer import TextOptions, render_text_layer
from bus_font_design import CLASSES
def compare(output: Path, old: Path):
output.mkdir(parents=True, exist_ok=True)
catalog = FontCatalog(output / "data")
columns = [(str(old),16,"OLD 16"), ("builtin:walnut-bus-8x16",16,"NEW 16"),
(str(old),32,"OLD 16 x2"), ("builtin:walnut-bus-16x32",32,"NEW 32")]
samples = ["高铁北站", "花鸟市场", "开往火车", "鹤警藏赢", "口日目田", "由甲申回", "国园圈围", "站铁路湖", "重量喜音", "人入大文", "0O1I", "l5S8", "B2Z6"]
sheet = Image.new("RGB", (1056, len(samples) * 148), "#111111")
draw = ImageDraw.Draw(sheet)
for y, text in enumerate(samples):
for x, (font, size, label) in enumerate(columns):
layer = render_text_layer(TextOptions(text, font=font, size=size), size=(64, 32), font_resolver=catalog.resolve)
scaled = layer.resize((256,128), Image.Resampling.NEAREST)
draw.text((x*264+2,y*148+2),label,fill="white")
sheet.paste(scaled,(x*264,y*148+18),scaled.getchannel('A'))
sheet.save(output / "four-way.png")
for kind, characters in CLASSES.items():
proof = Image.new("RGB", (len(characters)*48,96), "#111111")
labels = ImageDraw.Draw(proof)
for index, char in enumerate(characters):
layer = render_text_layer(TextOptions(char,font="builtin:walnut-bus-16x32",size=32),size=(16,32),font_resolver=catalog.resolve)
big=layer.resize((32,64),Image.Resampling.NEAREST)
labels.text((index*48,2),f"{ord(char):04X}",fill="white")
proof.paste(big,(index*48+4,20),big.getchannel('A'))
proof.save(output / f"class-{kind}.png")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--old", type=Path, required=True)
args = parser.parse_args()
compare(args.output,args.old)
@@ -0,0 +1,85 @@
"""Generate offline inspection sheets from the actual text-layer renderer."""
from __future__ import annotations
import argparse
from collections import defaultdict
import hashlib
import json
from pathlib import Path
import sys
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from app.display.text_renderer import TextOptions, render_text_layer
from app.fonts.store import FontCatalog
from build_bus_font import gb2312_codepoints
def proof(output: Path, reference: Path | None = None, height: int = 16) -> None:
output.mkdir(parents=True, exist_ok=True)
catalog = FontCatalog(output / "isolated-data")
width = height // 2
font_id = f"builtin:walnut-bus-{width}x{height}"
cps = sorted(gb2312_codepoints())
fingerprints = defaultdict(list)
grey, blank, boundary = [], [], []
densities = []
for cp in cps:
layer = render_text_layer(TextOptions(text=chr(cp), font=font_id, size=height),
size=(width, height), font_resolver=catalog.resolve)
alpha = layer.getchannel("A")
values = set(alpha.tobytes())
if values - {0, 255}:
grey.append(cp)
if not alpha.getbbox() and cp not in (32, 0x3000):
blank.append(cp)
if alpha.crop((width - 1, 0, width, height)).getbbox():
boundary.append(cp)
fingerprints[hashlib.sha256(alpha.tobytes()).hexdigest()].append(cp)
densities.append((sum(value != 0 for value in alpha.tobytes()), f"{cp:04X}"))
per_page = 256 if height == 32 else 512
scale = 2 if height == 32 else 3
cell_y = 84 if height == 32 else 64
for page, start in enumerate(range(0, len(cps), per_page)):
sheet = Image.new("RGB", (16 * 48, (per_page // 16) * cell_y), "#111111")
draw = ImageDraw.Draw(sheet)
for index, cp in enumerate(cps[start:start + per_page]):
x, y = (index % 16) * 48, (index // 16) * cell_y
draw.text((x + 1, y), f"{cp:04X}", fill="#aaaaaa")
layer = render_text_layer(TextOptions(text=chr(cp), font=font_id, size=height),
size=(width, height), font_resolver=catalog.resolve)
scaled = layer.resize((width * scale, height * scale), Image.Resampling.NEAREST)
sheet.paste(scaled, (x + 8, y + 14), scaled.getchannel("A"))
sheet.save(output / f"glyphs-{page + 1:02}.png")
samples = ["高铁北站21路", "花鸟市场公交", "开往火车站", "鹤警藏赢鬻", "口日目田由甲", "未末土士人入", "0O1Il5S8B", "ABCxyz0123", ",。!?:;"]
comparison = Image.new("RGB", (768, len(samples) * 160), "#111111")
draw = ImageDraw.Draw(comparison)
for row, text in enumerate(samples):
for column, font in enumerate([font_id] + ([str(reference)] if reference else [])):
layer = render_text_layer(TextOptions(text=text, font=font, size=height),
size=(64, height), font_resolver=catalog.resolve)
draw.text((column * 384 + 4, row * 160 + 2), f"BUS {width}x{height}" if column == 0 else f"REFERENCE {height}px", fill="white")
scaled = layer.resize((256, height * 4), Image.Resampling.NEAREST)
comparison.paste(scaled, (column * 384 + 4, row * 160 + 22), scaled.getchannel("A"))
comparison.save(output / "comparison.png")
result = {"glyph_count": len(cps), "grey": grey, "blank": blank,
"right_edge_lit": boundary,
"duplicates": [[f"{cp:04X}" for cp in group] for group in fingerprints.values() if len(group) > 1]}
result["densest_32"] = [{"codepoint": cp, "lit_pixels": count, "density": count / 128}
for count, cp in sorted(densities, reverse=True)[:32]]
for item in result["densest_32"]:
item["density"] = item["lit_pixels"] / (width * height)
(output / "audit.json").write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"grey": len(grey), "blank": len(blank), "right_edge_lit": len(boundary),
"duplicate_groups": len(result["duplicates"])}))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--output", required=True, type=Path)
parser.add_argument("--reference", type=Path)
parser.add_argument("--height", type=int, choices=(16, 32), default=16)
args = parser.parse_args()
proof(args.output, args.reference, args.height)
@@ -0,0 +1,135 @@
from __future__ import annotations
import hashlib
import base64
from io import BytesIO
import json
from pathlib import Path
import sys
from fastapi.testclient import TestClient
from fontTools.ttLib import TTFont
from PIL import Image
import pytest
from app.display.text_renderer import TextOptions, render_text_layer
from app.fonts.store import FontCatalog, FontValidationError
from app.main import create_app
from app.templates.store import TemplateStore, prepare_scene, render_scene_image
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "scripts"))
from build_bus_font import build, gb2312_codepoints
ID = "builtin:walnut-bus-8x16"
BUNDLE = ROOT / "app/fonts/bundled"
def test_reproducible_font_and_coverage(tmp_path):
manifest = build(tmp_path / "build")
assert manifest == json.loads((BUNDLE / "manifest.json").read_text(encoding="utf-8"))
for item in manifest["fonts"]:
assert (tmp_path / "build" / item["file"]).read_bytes() == (BUNDLE / item["file"]).read_bytes()
with TTFont(BUNDLE / item["file"]) as font:
cmap = font.getBestCmap()
assert gb2312_codepoints() <= cmap.keys()
assert len([cp for cp in cmap if 0x4E00 <= cp <= 0x9FFF]) == 6763
assert all(font["hmtx"][name][0] == 512 for name in cmap.values())
assert font["name"].getBestFamilyName() == item["label"]
@pytest.mark.parametrize("width,height", [(8,16), (16,32)])
def test_all_glyphs_binary_pixels_and_cell_bounds(tmp_path, width, height):
catalog = FontCatalog(tmp_path)
source = ROOT / f"font_sources/walnut-bus-{width}x{height}"
masters = json.loads((source / "glyphs.json").read_text(encoding="utf-8"))
revisions = json.loads((source / "revisions.json").read_text(encoding="utf-8"))
for cp in sorted(gb2312_codepoints()):
image = render_text_layer(TextOptions(chr(cp), font=f"builtin:walnut-bus-{width}x{height}", size=height),
size=(width + 8, height + 4), font_resolver=catalog.resolve)
alpha = image.getchannel("A")
assert set(alpha.tobytes()) <= {0, 255}, f"U+{cp:04X} grey"
if cp not in (32, 0x3000):
assert alpha.getbbox(), f"U+{cp:04X} blank"
assert not alpha.crop((width - 1, 0, width + 8, height + 4)).getbbox(), f"U+{cp:04X} exceeds ink columns"
assert not alpha.crop((0, height, width + 8, height + 4)).getbbox()
key = f"{cp:04X}"
rows = revisions[key]["rows"] if key in revisions else masters[key]
expected = bytes(255 if int(row, 16) & (1 << (width - 1 - x)) else 0 for row in rows for x in range(width))
assert alpha.crop((0, 0, width, height)).tobytes() == expected, f"U+{cp:04X} differs from master"
@pytest.mark.parametrize("height,factor", [(16,2), (16,3), (16,4), (32,2)])
def test_integer_scaling(height, factor, tmp_path):
catalog = FontCatalog(tmp_path)
identifier = f"builtin:walnut-bus-{height // 2}x{height}"
for char in "高铁路鹤警藏0O1Il5S8B口日目田由甲申回":
small = render_text_layer(TextOptions(char, font=identifier, size=height), size=(height // 2, height), font_resolver=catalog.resolve)
large = render_text_layer(TextOptions(char, font=identifier, size=height * factor),
size=(height // 2 * factor, height * factor), font_resolver=catalog.resolve)
assert large.tobytes() == small.resize(large.size, Image.Resampling.NEAREST).tobytes()
def test_catalog_restart_legacy_digest_and_corrupt_preservation(tmp_path):
catalog = FontCatalog(tmp_path)
registration = json.loads((BUNDLE / "manifest.json").read_text(encoding="utf-8"))
manifest = registration["fonts"][0]
face = catalog.resolve(ID)[0]
original = Path(face.path).read_bytes()
entry = next(item for item in catalog.document()["items"] if item["id"] == ID)
assert entry["source"] == "system" and "16px" in entry["label"]
assert catalog.resolve(f"user:{manifest['sha256']}:0") == (face,)
assert catalog.resolve(next(iter(registration["aliases"]))) == (face,)
assert FontCatalog(tmp_path).resolve(ID) == (face,)
Path(face.path).write_bytes(b"corrupt")
with pytest.raises(FontValidationError, match="preserved"):
FontCatalog(tmp_path)
assert Path(face.path).read_bytes() == b"corrupt"
assert hashlib.sha256(original).hexdigest() == manifest["sha256"]
def test_api_preview_placement_alignment_and_import(tmp_path):
client = TestClient(create_app(project_root=tmp_path, driver_kind="mock"))
assert any(item["id"] == ID for item in client.get("/api/fonts").json()["items"])
request = {"text": "高铁北站21路", "font": ID, "size": 16, "x": 0, "y": 0, "color": "#FF0000"}
response = client.post("/api/preview/text-layer", json=request)
assert response.status_code == 200
image = Image.open(BytesIO(response.content)).convert("RGBA")
for align, x in [("left", 8), ("center", 36), ("right", 64)]:
result = client.post("/api/preview/text-layer", json={**request, "x": x, "y": 16, "align": align})
assert result.status_code == 200
assert Image.open(BytesIO(result.content)).getchannel("A").getbbox()
imported = client.post("/api/fonts/import?filename=bus.ttf", content=(BUNDLE / "SimilarBus8x16.ttf").read_bytes(),
headers={"Content-Type": "application/octet-stream"})
assert imported.status_code == 200
assert imported.json()["fonts"][0]["id"] == ID
masks = [render_text_layer(TextOptions(char, font=ID, size=16), size=(8, 16),
font_resolver=client.app.state.font_catalog.resolve).getchannel("A").tobytes()
for char in "0O1Il5S8B"]
assert len(set(masks)) == len(masks)
def test_bus_template_reload_layer_order_clip_and_missing_glyph_fallback(tmp_path):
catalog = FontCatalog(tmp_path / "data")
legacy = dict(version=1, width=64, height=64,
pixelRgb=base64.b64encode(bytes([10, 20, 30]) * 4096).decode("ascii"),
elements=[dict(id="bus", type="text", text="高铁北站21路", font=ID,
size=16, x=0, y=8, align="left", color="#FF0000")])
scene = prepare_scene(legacy)
store = TemplateStore(tmp_path / "templates", font_resolver=catalog.resolve)
saved = store.create("bus", scene)
reloaded = TemplateStore(tmp_path / "templates", font_resolver=FontCatalog(tmp_path / "data").resolve).get(saved["id"])
assert reloaded["scene"] == scene
full = render_scene_image(scene, font_resolver=catalog.resolve)
assert full.getpixel((63, 63)) == (10, 20, 30)
scene["layers"][1]["elements"][0]["clip"] = [0, 0, 1, 1]
clipped = render_scene_image(scene, font_resolver=catalog.resolve)
assert clipped.getpixel((32, 12)) == (10, 20, 30)
assert clipped.tobytes() != full.tobytes()
from app.display.text_renderer import resolve_font_face
try:
face = resolve_font_face(ID, "高あ", font_resolver=catalog.resolve)
except ValueError as error:
assert "Unicode" in str(error) # hosts without a covering fallback fail clearly
else:
assert face != catalog.resolve(ID)[0] # whole sequence, never mixed fragments
@@ -0,0 +1,112 @@
from __future__ import annotations
import base64
import hashlib
import json
from pathlib import Path
import pytest
from app.fonts.store import FontCatalog, FontValidationError
from app.templates.store import TemplateStore, prepare_scene
from app.animations.store import AnimationStore
ID = "builtin:walnut-bus-8x16"
def scene(font=ID):
return prepare_scene(dict(version=1, width=64, height=64,
pixelRgb=base64.b64encode(bytes(64*64*3)).decode("ascii"),
elements=[dict(id="text",type="text",text="高铁路",font=font,size=16,x=0,y=8,align="left",color="#FFFFFF")]))
class ChangingFont:
def __init__(self, catalog):
self.catalog, self.version = catalog, 1
def render_version(self, identifier):
return str(self.version)*64 if identifier == ID else None
def resolve(self, identifier):
return self.catalog.resolve(ID if self.version == 1 else "builtin:walnut-bus-16x32")
def test_template_cache_rebuilt_without_revision_or_record_change(tmp_path):
owner=ChangingFont(FontCatalog(tmp_path))
store=TemplateStore(tmp_path, font_resolver=owner.resolve)
item=store.create("bus",scene())
path=store.thumbnail_path(item["id"])
record=store._record_path(item["id"])
before_bytes, before_record = path.read_bytes(), record.read_bytes()
assert "font_v=" in item["thumbnail_url"]
owner.version=2
restarted=TemplateStore(tmp_path,font_resolver=owner.resolve)
updated=restarted.list()["templates"][0]
assert updated["revision"] == item["revision"]
assert record.read_bytes() == before_record
assert path.read_bytes() != before_bytes
assert updated["thumbnail_url"] != item["thumbnail_url"]
stable=path.stat().st_mtime_ns
TemplateStore(tmp_path,font_resolver=owner.resolve)
assert path.stat().st_mtime_ns == stable
def test_animation_cache_rebuilt_without_scene_or_revision_change(tmp_path):
owner=ChangingFont(FontCatalog(tmp_path))
store=AnimationStore(tmp_path,font_resolver=owner.resolve)
item=store.create_from_frames("bus",[dict(scene=scene(),duration_ms=100)])
frame=store.get(item["id"])["frames"][0]
stored_frame=store._read(item["id"])["frames"][0]
path=store._thumb_dir(item["id"])/stored_frame["thumbnail"]
record=store._path(item["id"])
before_bytes, before_record = path.read_bytes(), record.read_bytes()
owner.version=2
restarted=AnimationStore(tmp_path,font_resolver=owner.resolve)
updated=restarted.get(item["id"])
assert updated["revision"] == item["revision"]
assert record.read_bytes() == before_record
assert path.read_bytes() != before_bytes
assert updated["frames"][0]["thumbnail_url"] != frame["thumbnail_url"]
stable=path.stat().st_mtime_ns
AnimationStore(tmp_path,font_resolver=owner.resolve)
assert path.stat().st_mtime_ns == stable
def test_unrelated_thumbnails_untouched(tmp_path):
owner=ChangingFont(FontCatalog(tmp_path))
store=TemplateStore(tmp_path,font_resolver=owner.resolve)
item=store.create("normal",scene("default"))
path=store.thumbnail_path(item["id"])
stable=path.read_bytes(),path.stat().st_mtime_ns
assert "font_v=" not in item["thumbnail_url"]
owner.version=2
TemplateStore(tmp_path,font_resolver=owner.resolve)
assert (path.read_bytes(),path.stat().st_mtime_ns) == stable
def test_exact_obsolete_retirement_and_corruption_preservation(tmp_path, monkeypatch):
import app.fonts.store as module
bundle=Path(module.__file__).parent/'bundled'
registration=json.loads((bundle/'manifest.json').read_text(encoding='utf-8'))
old_payload=b'old-font-fixture'
old_digest=hashlib.sha256(old_payload).hexdigest()
registration['obsolete']=[old_digest]
registration['aliases']={f'user:{old_digest}:0':ID}
# Substitute only the registration reader, retaining real bundled font
# files and checksum validation; fixture files never need old private state.
real_read=Path.read_text
def read(path,*args,**kwargs):
if path == bundle/'manifest.json':return json.dumps(registration)
return real_read(path,*args,**kwargs)
monkeypatch.setattr(Path,'read_text',read)
root=tmp_path/'fonts';root.mkdir()
old=root/f'{old_digest}.font';old.write_bytes(old_payload)
unrelated=root/'unregistered.font';unrelated.write_bytes(b'keep')
catalog=FontCatalog(tmp_path)
assert not old.exists()
assert unrelated.read_bytes() == b'keep'
assert catalog.resolve(f'user:{old_digest}:0') == catalog.resolve(ID)
old.write_bytes(b'corrupt')
with pytest.raises(FontValidationError,match='preserved'):
FontCatalog(tmp_path)
assert old.read_bytes() == b'corrupt'
@@ -0,0 +1,81 @@
from concurrent.futures import ThreadPoolExecutor
import json
import pytest
from fastapi.testclient import TestClient
from app.fonts.store import FontCatalog, FontValidationError
from app.main import create_app
def test_api_validation_and_persistence(tmp_path):
app = create_app(project_root=tmp_path, driver_kind="mock")
client = TestClient(app)
items = client.get("/api/fonts").json()["items"]
assert client.get("/api/fonts").json()["favorite_ids"] == []
ids = [item["id"] for item in items[:3]]
for identifier in ids:
assert client.put("/api/fonts/favorite", json={"id": identifier, "favorite": True}).status_code == 200
path = app.state.font_catalog.root / "favorites.json"
before = path.read_bytes()
client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": True})
assert path.read_bytes() == before
assert FontCatalog(path.parent.parent).document()["favorite_ids"] == ids
for payload in ({"id": "default", "favorite": "true"}, {"id": "default", "favorite": 1}, {"id": "", "favorite": True}, {"id": "default", "favorite": True, "extra": 1}):
assert client.put("/api/fonts/favorite", json=payload).status_code == 422
assert client.put("/api/fonts/favorite", json={"id": "missing", "favorite": True}).status_code == 404
assert client.put("/api/fonts/favorite", json={"id": "missing", "favorite": False}).status_code == 200
client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": False})
assert client.get("/api/fonts").json()["favorite_ids"] == ids[1:]
def test_concurrent_merge_and_write_failure(tmp_path, monkeypatch):
catalog = FontCatalog(tmp_path)
ids = [item["id"] for item in catalog.document()["items"][:3]]
with ThreadPoolExecutor(max_workers=3) as pool:
list(pool.map(lambda identifier: catalog.set_favorite(identifier, True), ids))
assert set(catalog.document()["favorite_ids"]) == set(ids)
before = (catalog.root / "favorites.json").read_bytes()
def fail(*args):
raise OSError("injected write failure")
monkeypatch.setattr("app.fonts.store.atomic_write_bytes", fail)
with pytest.raises(OSError):
catalog.set_favorite(ids[0], False)
assert set(catalog.document()["favorite_ids"]) == set(ids)
assert (catalog.root / "favorites.json").read_bytes() == before
@pytest.mark.parametrize("content", [b"broken", b'{"schema_version":2,"favorite_ids":[]}', b'{"schema_version":true,"favorite_ids":[]}', b'{"schema_version":1,"favorite_ids":["x","x"]}', b'{"schema_version":1,"favorite_ids":[],"extra":1}'])
def test_invalid_file_preserved(tmp_path, content):
(tmp_path / "fonts").mkdir()
path = tmp_path / "fonts/favorites.json"
path.write_bytes(content)
with pytest.raises(FontValidationError):
FontCatalog(tmp_path)
assert path.read_bytes() == content
def test_stale_ids_preserved_and_removable(tmp_path):
catalog = FontCatalog(tmp_path)
path = catalog.root / "favorites.json"
path.write_text(json.dumps({"schema_version": 1, "favorite_ids": ["stale"]}), encoding="utf-8")
catalog = FontCatalog(tmp_path)
assert catalog.document()["favorite_ids"] == ["stale"]
assert not any(item["id"] == "stale" for item in catalog.document()["items"])
assert catalog.set_favorite("stale", False) == []
def test_same_family_styles_are_independent(tmp_path):
from test_fonts import build_test_font
app = create_app(project_root=tmp_path, driver_kind="mock")
client = TestClient(app)
ids = []
for style in ["Bold", "Italic"]:
response = client.post("/api/fonts/import?filename=style.ttf", content=build_test_font("Same Family", style), headers={"Content-Type": "application/octet-stream"})
assert response.status_code == 201
ids.append(response.json()["fonts"][0]["id"])
client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": True})
assert client.get("/api/fonts").json()["favorite_ids"] == [ids[0]]
client.put("/api/fonts/favorite", json={"id": ids[1], "favorite": True})
client.put("/api/fonts/favorite", json={"id": ids[0], "favorite": False})
assert client.get("/api/fonts").json()["favorite_ids"] == [ids[1]]
@@ -21,7 +21,7 @@ test("font catalog normalization, grouping, and search are stable", () => {
{ id: "bad", source: "unknown" },
]);
assert.deepEqual(items.map((item) => item.id), ["default", "user:abc:0", "system:def"]);
assert.deepEqual(groupFontItems(items).map((group) => group.label), ["自动字体", "已导入字体", "系统字体"]);
assert.deepEqual(groupFontItems(items).map((group) => group.label), ["收藏字体", "自动字体", "已导入字体", "系统字体"]);
assert.deepEqual(filterFontItems(items, "宋体").map((item) => item.id), ["user:abc:0"]);
assert.deepEqual(filterFontItems(items, "bold").map((item) => item.id), ["system:def"]);
assert.equal(filterFontItems(items, "没有").length, 0);
@@ -39,3 +39,16 @@ test("font picker implements commit-only searchable combobox keyboard behavior",
assert.match(source, /onCommit\?\.\(item\)/);
assert.doesNotMatch(source, /onCommit\?\.\([^)]*query/);
});
test("favorites duplicate the original font faces and preserve catalog order", () => {
const items = normalizeFontItems([
{ id: "default", source: "automatic" },
{ id: "bold", family: "Same", style: "Bold", source: "system" },
{ id: "italic", family: "Same", style: "Italic", source: "system" },
]);
const groups = groupFontItems(items, ["italic", "bold", "missing"]);
assert.deepEqual(groups[0].items.map(item => item.id), ["bold", "italic"]);
assert.deepEqual(groups[2].items.map(item => item.id), ["bold", "italic"]);
assert.equal(groupFontItems(items)[0].items.length, 0);
assert.deepEqual(groupFontItems(filterFontItems(items, "Italic"), ["italic"])[0].items.map(item => item.id), ["italic"]);
});
+4 -4
View File
@@ -89,8 +89,8 @@ def test_font_catalog_import_duplicate_restart_and_render(tmp_path: Path) -> Non
assert str(tmp_path) not in created.text
stored = list((tmp_path / "data" / "fonts").glob("*.font"))
assert len(stored) == 1
assert stored[0].stem in imported["id"]
assert len(stored) == 3 # imported test face plus two bundled bus fonts
assert any(path.stem in imported["id"] for path in stored)
duplicate = client.post(
"/api/fonts/import?filename=renamed.otf",
@@ -99,7 +99,7 @@ def test_font_catalog_import_duplicate_restart_and_render(tmp_path: Path) -> Non
)
assert duplicate.status_code == 200
assert duplicate.json() == {"created": False, "fonts": [imported]}
assert len(list((tmp_path / "data" / "fonts").glob("*.font"))) == 1
assert len(list((tmp_path / "data" / "fonts").glob("*.font"))) == 3
preview = client.post("/api/preview/text-layer", json={
"text": "AB",
@@ -163,7 +163,7 @@ def test_font_import_rejects_type_content_and_size_without_residue(tmp_path: Pat
)
assert oversized.status_code == 413
assert not list(root.glob(".upload-*.tmp"))
assert not list(root.glob("*.font"))
assert len(list(root.glob("*.font"))) == 2 # bundled bus fonts are preserved
def test_invalid_persisted_font_is_preserved_and_reported(tmp_path: Path) -> None: