Add bundled bus fonts and shared font favorites
This commit is contained in:
@@ -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.
|
||||
Binary file not shown.
Binary file not shown.
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
Binary file not shown.
@@ -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
|
||||
+395618
File diff suppressed because it is too large
Load Diff
@@ -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"]);
|
||||
});
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user