mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
feat(fonts): add dynamic font selection and font manager improvements (#232)
* feat(fonts): add dynamic font selection and font manager improvements - Add font-selector widget for dynamic font selection in plugin configs - Enhance /api/v3/fonts/catalog with filename, display_name, and type - Add /api/v3/fonts/preview endpoint for server-side font rendering - Add /api/v3/fonts/<family> DELETE endpoint with system font protection - Fix /api/v3/fonts/upload to actually save uploaded font files - Update font manager tab with dynamic dropdowns, server-side preview, and font deletion - Add new BDF fonts: 6x10, 6x12, 6x13, 7x13, 7x14, 8x13, 9x15, 9x18, 10x20 (with bold/oblique variants) - Add tom-thumb, helvR12, clR6x12, texgyre-27 fonts Plugin authors can use x-widget: "font-selector" in schemas to enable dynamic font selection that automatically shows all available fonts. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): security fixes and code quality improvements - Fix README.md typos and add language tags to code fences - Remove duplicate delete_font function causing Flask endpoint collision - Add safe integer parsing for size parameter in preview endpoint - Fix path traversal vulnerability in /fonts/preview endpoint - Fix path traversal vulnerability in /fonts/<family> DELETE endpoint - Fix XSS vulnerability in fonts.html by using DOM APIs instead of innerHTML - Move baseUrl to shared scope to fix ReferenceError in multiple functions Security improvements: - Validate font filenames reject path separators and '..' - Validate paths are within fonts_dir before file operations - Use textContent and data attributes instead of inline onclick handlers - Restrict file extensions to known font types Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): address code issues and XSS vulnerabilities - Move `import re` to module level, remove inline imports - Remove duplicate font_file assignment in upload_font() - Remove redundant validation with inconsistent allowed extensions - Remove redundant PathLib import, use already-imported Path - Fix XSS vulnerabilities in fonts.html by using DOM APIs instead of innerHTML with template literals for user-controlled data Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): add size limits to font preview endpoint Add input validation to prevent DoS via large image generation: - MAX_TEXT_CHARS (100): Limit text input length - MAX_TEXT_LINES (3): Limit number of newlines - MAX_DIM (1024): Limit max width/height - MAX_PIXELS (500000): Limit total pixel count Validates text early before processing and checks computed dimensions after bbox calculation but before image allocation. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): improve error handling, catalog keys, and BDF preview - Add structured logging for cache invalidation failures instead of silent pass (FontUpload, FontDelete, FontCatalog contexts) - Use filename as unique catalog key to prevent collisions when multiple font files share the same family_name from metadata - Return explicit error for BDF font preview instead of showing misleading preview with default font Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): address nitpick issues in font management Frontend (fonts.html): - Remove unused escapeHtml function (dead code) - Add max-attempts guard (50 retries) to initialization loop - Add response.ok checks before JSON parsing in deleteFont, addFontOverride, deleteFontOverride, uploadSelectedFonts - Use is_system flag from API instead of hardcoded client-side list Backend (api_v3.py): - Move SYSTEM_FONTS to module-level frozenset for single source of truth - Add is_system flag to font catalog entries - Simplify delete_font system font check using frozenset lookup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): align frontend upload validation with backend - Add .otf to accepted file extensions (HTML accept attribute, JS filter) - Update validation regex to allow hyphens (matching backend) - Preserve hyphens in auto-generated font family names - Update UI text to reflect all supported formats Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): fix lint errors and missing variable - Remove unused exception binding in set_cached except block - Define font_family_lower before case-insensitive fallback loop - Add response.ok check to font preview fetch (consistent with other handlers) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): address nitpick code quality issues - Add return type hints to get_font_preview and delete_font endpoints - Catch specific PIL exceptions (IOError/OSError) when loading fonts - Replace innerHTML with DOM APIs for trash icon (consistency) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix(fonts): remove unused exception bindings in cache-clearing blocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
140567
assets/fonts/10x20.bdf
Normal file
140567
assets/fonts/10x20.bdf
Normal file
File diff suppressed because it is too large
Load Diff
31042
assets/fonts/6x10.bdf
Normal file
31042
assets/fonts/6x10.bdf
Normal file
File diff suppressed because it is too large
Load Diff
86121
assets/fonts/6x12.bdf
Normal file
86121
assets/fonts/6x12.bdf
Normal file
File diff suppressed because it is too large
Load Diff
82452
assets/fonts/6x13.bdf
Normal file
82452
assets/fonts/6x13.bdf
Normal file
File diff suppressed because it is too large
Load Diff
25672
assets/fonts/6x13B.bdf
Normal file
25672
assets/fonts/6x13B.bdf
Normal file
File diff suppressed because it is too large
Load Diff
15432
assets/fonts/6x13O.bdf
Normal file
15432
assets/fonts/6x13O.bdf
Normal file
File diff suppressed because it is too large
Load Diff
64553
assets/fonts/7x13.bdf
Normal file
64553
assets/fonts/7x13.bdf
Normal file
File diff suppressed because it is too large
Load Diff
20093
assets/fonts/7x13B.bdf
Normal file
20093
assets/fonts/7x13B.bdf
Normal file
File diff suppressed because it is too large
Load Diff
16653
assets/fonts/7x13O.bdf
Normal file
16653
assets/fonts/7x13O.bdf
Normal file
File diff suppressed because it is too large
Load Diff
54128
assets/fonts/7x14.bdf
Normal file
54128
assets/fonts/7x14.bdf
Normal file
File diff suppressed because it is too large
Load Diff
21221
assets/fonts/7x14B.bdf
Normal file
21221
assets/fonts/7x14B.bdf
Normal file
File diff suppressed because it is too large
Load Diff
74092
assets/fonts/8x13.bdf
Normal file
74092
assets/fonts/8x13.bdf
Normal file
File diff suppressed because it is too large
Load Diff
22852
assets/fonts/8x13B.bdf
Normal file
22852
assets/fonts/8x13B.bdf
Normal file
File diff suppressed because it is too large
Load Diff
25932
assets/fonts/8x13O.bdf
Normal file
25932
assets/fonts/8x13O.bdf
Normal file
File diff suppressed because it is too large
Load Diff
105126
assets/fonts/9x15.bdf
Normal file
105126
assets/fonts/9x15.bdf
Normal file
File diff suppressed because it is too large
Load Diff
37168
assets/fonts/9x15B.bdf
Normal file
37168
assets/fonts/9x15B.bdf
Normal file
File diff suppressed because it is too large
Load Diff
119182
assets/fonts/9x18.bdf
Normal file
119182
assets/fonts/9x18.bdf
Normal file
File diff suppressed because it is too large
Load Diff
19082
assets/fonts/9x18B.bdf
Normal file
19082
assets/fonts/9x18B.bdf
Normal file
File diff suppressed because it is too large
Load Diff
42
assets/fonts/AUTHORS
Normal file
42
assets/fonts/AUTHORS
Normal file
@@ -0,0 +1,42 @@
|
||||
The identity of the designer(s) of the original ASCII repertoire and
|
||||
the later Latin-1 extension of the misc-fixed BDF fonts appears to
|
||||
have been lost in history. (It is likely that many of these 7-bit
|
||||
ASCII fonts were created in the early or mid 1980s as part of MIT's
|
||||
Project Athena, or at its industrial partner, DEC.)
|
||||
|
||||
In 1997, Markus Kuhn at the University of Cambridge Computer
|
||||
Laboratory initiated and headed a project to extend the misc-fixed BDF
|
||||
fonts to as large a subset of Unicode/ISO 10646 as is feasible for
|
||||
each of the available font sizes, as part of a wider effort to
|
||||
encourage users of POSIX systems to migrate from ISO 8859 to UTF-8.
|
||||
|
||||
Robert Brady <rwb197@ecs.soton.ac.uk> and Birger Langkjer
|
||||
<birger.langkjer@image.dk> contributed thousands of glyphs and made
|
||||
very substantial contributions and improvements on almost all fonts.
|
||||
Constantine Stathopoulos <cstath@irismedia.gr> contributed all the
|
||||
Greek characters. Markus Kuhn <http://www.cl.cam.ac.uk/~mgk25/> did
|
||||
most 6x13 glyphs and the italic fonts and provided many more glyphs,
|
||||
coordination, and quality assurance for the other fonts. Mark Leisher
|
||||
<mleisher@crl.nmsu.edu> contributed to 6x13 Armenian, Georgian, the
|
||||
first version of Latin Extended Block A and some Cyrillic. Serge V.
|
||||
Vakulenko <vak@crox.net.kiae.su> donated the original Cyrillic glyphs
|
||||
from his 6x13 ISO 8859-5 font. Nozomi Ytow <nozomi@biol.tsukuba.ac.jp>
|
||||
contributed 6x13 halfwidth Katakana. Henning Brunzel
|
||||
<hbrunzel@meta-systems.de> contributed glyphs to 10x20.bdf. Theppitak
|
||||
Karoonboonyanan <thep@linux.thai.net> contributed Thai for 7x13,
|
||||
7x13B, 7x13O, 7x14, 7x14B, 8x13, 8x13B, 8x13O, 9x15, 9x15B, and 10x20.
|
||||
Karl Koehler <koehler@or.uni-bonn.de> contributed Arabic to 9x15,
|
||||
9x15B, and 10x20 and Roozbeh Pournader <roozbeh@sharif.ac.ir> and
|
||||
Behdad Esfahbod revised and extended Arabic in 10x20. Raphael Finkel
|
||||
<raphael@cs.uky.edu> revised Hebrew/Yiddish in 10x20. Jungshik Shin
|
||||
<jshin@pantheon.yale.edu> prepared 18x18ko.bdf. Won-kyu Park
|
||||
<wkpark@chem.skku.ac.kr> prepared the Hangul glyphs used in 12x13ja.
|
||||
Janne V. Kujala <jvk@iki.fi> contributed 4x6. Daniel Yacob
|
||||
<perl@geez.org> revised some Ethiopic glyphs. Ted Zlatanov
|
||||
<tzz@lifelogs.com> did some 7x14. Mikael Öhman <micketeer@gmail.com>
|
||||
worked on 6x12.
|
||||
|
||||
The fonts are still maintained by Markus Kuhn and the original
|
||||
distribution can be found at:
|
||||
|
||||
http://www.cl.cam.ac.uk/~mgk25/ucs-fonts.html
|
||||
369
assets/fonts/README
Normal file
369
assets/fonts/README
Normal file
@@ -0,0 +1,369 @@
|
||||
|
||||
Unicode versions of the X11 "misc-fixed-*" fonts
|
||||
------------------------------------------------
|
||||
|
||||
Markus Kuhn <http://www.cl.cam.ac.uk/~mgk25/> -- 2008-04-21
|
||||
|
||||
|
||||
This package contains the X Window System bitmap fonts
|
||||
|
||||
-Misc-Fixed-*-*-*--*-*-*-*-C-*-ISO10646-1
|
||||
|
||||
These are Unicode (ISO 10646-1) extensions of the classic ISO 8859-1
|
||||
X11 terminal fonts that are widely used with many X11 applications
|
||||
such as xterm, emacs, etc.
|
||||
|
||||
COVERAGE
|
||||
--------
|
||||
|
||||
None of these fonts covers Unicode completely. Complete coverage
|
||||
simply would not make much sense here. Unicode 5.1 contains over
|
||||
100000 characters, and the large majority of them are
|
||||
Chinese/Japanese/Korean Han ideographs (~70000) and Korean Hangul
|
||||
Syllables (~11000) that cannot adequately be displayed in the small
|
||||
pixel sizes of the fixed fonts. Similarly, Arabic characters are
|
||||
difficult to fit nicely together with European characters into the
|
||||
fixed character cells and X11 lacks the ligature substitution
|
||||
mechanisms required for using Indic scripts.
|
||||
|
||||
Therefore these fonts primarily attempt to cover Unicode subsets that
|
||||
fit together with European scripts. This includes the Latin, Greek,
|
||||
Cyrillic, Armenian, Georgian, and Hebrew scripts, plus a lot of
|
||||
linguistic, technical and mathematical symbols. Some of the fixed
|
||||
fonts now also cover Arabic, Thai, Ethiopian, halfwidth Katakana, and
|
||||
some other non-European scripts.
|
||||
|
||||
We have defined 3 different target character repertoires (ISO 10646-1
|
||||
subsets) that the various fonts were checked against for minimal
|
||||
guaranteed coverage:
|
||||
|
||||
TARGET1 617 characters
|
||||
Covers all characters of ISO 8859 part 1-5,7-10,13-16,
|
||||
CEN MES-1, ISO 6937, Microsoft CP1251/CP1252, DEC VT100
|
||||
graphics symbols, and the replacement and default
|
||||
character. It is intended for small bold, italic, and
|
||||
proportional fonts, for which adding block graphics
|
||||
characters would make little sense. This repertoire
|
||||
covers the following ISO 10646-1:2000 collections
|
||||
completely: 1-3, 8, 12.
|
||||
|
||||
TARGET2 886 characters
|
||||
Adds to TARGET1 the characters of the Adobe/Microsoft
|
||||
Windows Glyph List 4 (WGL4), plus a selected set of
|
||||
mathematical characters (covering most of ISO 31-11
|
||||
high-school level math symbols) and some combining
|
||||
characters. It is intended to be covered by all normal
|
||||
"fixed" fonts and covers all European IBM, Microsoft, and
|
||||
Macintosh character sets. This repertoire covers the
|
||||
following ISO 10646-1:2000 (including Amd 1:2002)
|
||||
collections completely: 1-3, 8, 12, 33, 45.
|
||||
|
||||
TARGET3 3282 characters
|
||||
|
||||
Adds to TARGET2 all characters of all European scripts
|
||||
(Latin, Greek, Cyrillic, Armenian, Georgian), all
|
||||
phonetic alphabet symbols, many mathematical symbols
|
||||
(including all those available in LaTeX), all typographic
|
||||
punctuation, all box-drawing characters, control code
|
||||
pictures, graphical shapes and some more that you would
|
||||
expect in a very comprehensive Unicode 4.0 font for
|
||||
European users. It is intended for some of the more
|
||||
useful and more widely used normal "fixed" fonts. This
|
||||
repertoire is, with two exceptions, a superset of all
|
||||
graphical characters in CEN MES-3A and covers the
|
||||
following ISO 10646-1:2000 (including Amd 1:2002)
|
||||
collections completely: 1-12, 27, 30-31, 32 (only
|
||||
graphical characters), 33-42, 44-47, 63, 65, 70 (only
|
||||
graphical characters).
|
||||
|
||||
[The two MES-3A characters deliberately omitted are the
|
||||
angle bracket characters U+2329 and U+232A. ISO and CEN
|
||||
appears to have included these into collection 40 and
|
||||
MES-3A by accident, because there they are the only
|
||||
characters in the Unicode EastAsianWidth "wide" class.]
|
||||
|
||||
CURRENT STATUS:
|
||||
|
||||
6x13.bdf 8x13.bdf 9x15.bdf 9x18.bdf 10x20.bdf:
|
||||
|
||||
Complete (TARGET3 reached and checked)
|
||||
|
||||
5x7.bdf 5x8.bdf 6x9.bdf 6x10.bdf 6x12.bdf 7x13.bdf 7x14.bdf clR6x12.bdf:
|
||||
|
||||
Complete (TARGET2 reached and checked)
|
||||
|
||||
6x13B.bdf 7x13B.bdf 7x14B.bdf 8x13B.bdf 9x15B.bdf 9x18B.bdf:
|
||||
|
||||
Complete (TARGET1 reached and checked)
|
||||
|
||||
6x13O.bdf 7x13O.bdf 8x13O.bdf
|
||||
|
||||
Complete (TARGET1 minus Hebrew and block graphics)
|
||||
|
||||
[None of the above fonts contains any character that has in Unicode
|
||||
the East Asian Width Property "W" or "F" assigned. This way, the
|
||||
desired combination of "half-width" and "full-width" glyphs can be
|
||||
achieved easily. Most font mechanisms display a character that is not
|
||||
covered in a font by using a glyph from another font that appears
|
||||
later in a priority list, which can be arranged to be a "full-width"
|
||||
font.]
|
||||
|
||||
The supplement package
|
||||
|
||||
http://www.cl.cam.ac.uk/~mgk25/download/ucs-fonts-asian.tar.gz
|
||||
|
||||
contains the following additional square fonts with Han characters for
|
||||
East Asian users:
|
||||
|
||||
12x13ja.bdf:
|
||||
|
||||
Covers TARGET2, JIS X 0208, Hangul, and a few more. This font is
|
||||
primarily intended to provide Japanese full-width Hiragana,
|
||||
Katakana, and Kanji for applications that take the remaining
|
||||
("halfwidth") characters from 6x13.bdf. The Greek lowercase
|
||||
characters in it are still a bit ugly and will need some work.
|
||||
|
||||
18x18ja.bdf:
|
||||
|
||||
Covers all JIS X 0208, JIS X 0212, GB 2312-80, KS X 1001:1992,
|
||||
ISO 8859-1,2,3,4,5,7,9,10,15, CP437, CP850 and CP1252 characters,
|
||||
plus a few more, where priority was given to Japanese han style
|
||||
variants. This font should have everything needed to cover the
|
||||
full ISO-2022-JP-2 (RFC 1554) repertoire. This font is primarily
|
||||
intended to provide Japanese full-width Hiragana, Katakana, and
|
||||
Kanji for applications that take the remaining ("halfwidth")
|
||||
characters from 9x18.bdf.
|
||||
|
||||
18x18ko.bdf:
|
||||
|
||||
Covers the same repertoire as 18x18ja plus full coverage of all
|
||||
Hangul syllables and priority was given to Hanja glyphs in the
|
||||
unified CJK area as they are used for writing Korean.
|
||||
|
||||
The 9x18 and 6x12 fonts are recommended for use with overstriking
|
||||
combining characters.
|
||||
|
||||
Bug reports, suggestions for improvement, and especially contributed
|
||||
extensions are very welcome!
|
||||
|
||||
INSTALLATION
|
||||
------------
|
||||
|
||||
You install the fonts under Unix roughly like this (details depending
|
||||
on your system of course):
|
||||
|
||||
System-wide installation (root access required):
|
||||
|
||||
cd submission/
|
||||
make
|
||||
su
|
||||
mv -b *.pcf.gz /usr/lib/X11/fonts/misc/
|
||||
cd /usr/lib/X11/fonts/misc/
|
||||
mkfontdir
|
||||
xset fp rehash
|
||||
|
||||
Alternative: Installation in your private user directory:
|
||||
|
||||
cd submission/
|
||||
make
|
||||
mkdir -p ~/local/lib/X11/fonts/
|
||||
mv *.pcf.gz ~/local/lib/X11/fonts/
|
||||
cd ~/local/lib/X11/fonts/
|
||||
mkfontdir
|
||||
xset +fp ~/local/lib/X11/fonts (put this last line also in ~/.xinitrc)
|
||||
|
||||
Now you can have a look at say the 6x13 font with the command
|
||||
|
||||
xfd -fn '-misc-fixed-medium-r-semicondensed--13-120-75-75-c-60-iso10646-1'
|
||||
|
||||
If you want to have short names for the Unicode fonts, you can also
|
||||
append the fonts.alias file to that in the directory where you install
|
||||
the fonts, call "mkfontdir" and "xset fp rehash" again, and then you
|
||||
can also write
|
||||
|
||||
xfd -fn 6x13U
|
||||
|
||||
Note: If you use an old version of xfontsel, you might notice that it
|
||||
treats every font that contains characters >0x00ff as a Japanese JIS
|
||||
font and therefore selects inappropriate sample characters for display
|
||||
of ISO 10646-1 fonts. An updated xfontsel version with this bug fixed
|
||||
comes with XFree86 4.0 / X11R6.8 or newer.
|
||||
|
||||
If you use the Exceed X server on Microsoft Windows, then you will
|
||||
have to convert the BDF files into Microsoft FON files using the
|
||||
"Compile Fonts" function of Exceed xconfig. See the file exceed.txt
|
||||
for more information.
|
||||
|
||||
There is one significant efficiency problem that X11R6 has with the
|
||||
sparsely populated ISO10646-1 fonts. X11 transmits and allocates 12
|
||||
bytes with the XFontStruct data structure for the difference between
|
||||
the lowest and the highest code value found in a font, no matter
|
||||
whether the code positions in between are used for characters or not.
|
||||
Even a tiny font that contains only two glyphs at positions 0x0000 and
|
||||
0xfffd causes 12 bytes * 65534 codes = 786 kbytes to be requested and
|
||||
stored by the client. Since all the ISO10646-1 BDF files provided in
|
||||
this package contain characters in the U+00xx (ASCII) and U+ffxx
|
||||
(ligatures, etc.) range, all of them would result in 786 kbyte large
|
||||
XCharStruct arrays in the per_char array of the corresponding
|
||||
XFontStruct (even for CharCell fonts!) when loaded by an X client.
|
||||
Until this problem is fixed by extending the X11 font protocol and
|
||||
implementation, non-CJK ISO10646-1 fonts that lack the (anyway not
|
||||
very interesting) characters above U+31FF seem to be the best
|
||||
compromise. The bdftruncate.pl program in this package can be used to
|
||||
deactivate any glyphs above a threshold code value in BDF files. This
|
||||
way, we get relatively memory-economic ISO10646-1 fonts that cause
|
||||
"only" 150 kbyte large XCharStruct arrays to be allocated. The
|
||||
deactivated glyphs are still present in the BDF files, but with an
|
||||
encoding value of -1 that causes them to be ignored.
|
||||
|
||||
The ISO10646-1 fonts can not only be used directly by Unicode aware
|
||||
software, they can also be used to create any 8-bit font. The
|
||||
ucs2any.pl Perl script converts a ISO10646-1 BDF font into a BDF font
|
||||
file with some different encoding. For instance the command
|
||||
|
||||
perl ucs2any.pl 6x13.bdf MAPPINGS/8859-7.TXT ISO8859-7
|
||||
|
||||
will generate the file 6x13-ISO8859-7.bdf according to the 8859-7.TXT
|
||||
Latin/Greek mapping table, which available from
|
||||
<ftp://ftp.unicode.org/Public/MAPPINGS/>. [The shell script
|
||||
./map_fonts automatically generates a subdirectory derived-fonts/ with
|
||||
many *.bdf and *.pcf.gz 8-bit versions of all the
|
||||
-misc-fixed-*-iso10646-1 fonts.]
|
||||
|
||||
When you do a "make" in the submission/ subdirectory as suggested in
|
||||
the installation instructions above, this will generate exactly the
|
||||
set of fonts that have been submitted to the XFree86 project for
|
||||
inclusion into XFree86 4.0. These consists of all the ISO10646-1 fonts
|
||||
processed with "bdftruncate.pl U+3200" plus a selected set of derived
|
||||
8-bit fonts generated with ucs2any.pl.
|
||||
|
||||
Every font comes with a *.repertoire-utf8 file that lists all the
|
||||
characters in this font.
|
||||
|
||||
|
||||
CONTRIBUTING
|
||||
------------
|
||||
|
||||
If you want to help me in extending or improving the fonts, or if you
|
||||
want to start your own ISO 10646-1 font project, you will have to edit
|
||||
BDF font files. This is most comfortably done with the gbdfed font
|
||||
editor (version 1.3 or higher), which is available from
|
||||
|
||||
http://crl.nmsu.edu/~mleisher/gbdfed.html
|
||||
|
||||
Once you are familiar with gbdfed, you will notice that it is no
|
||||
problem to design up to 100 nice characters per hour (even more if
|
||||
only placing accents is involved).
|
||||
|
||||
Information about other X11 font tools and Unicode fonts for X11 in
|
||||
general can be found on
|
||||
|
||||
http://www.cl.cam.ac.uk/~mgk25/ucs-fonts.html
|
||||
|
||||
The latest version of this package is available from
|
||||
|
||||
http://www.cl.cam.ac.uk/~mgk25/download/ucs-fonts.tar.gz
|
||||
|
||||
If you want to contribute, then get the very latest version of this
|
||||
package, check which glyphs are still missing or inappropriate for
|
||||
your needs, and send me whatever you had the time to add and fix. Just
|
||||
email me the extended BDF-files back, or even better, send me a patch
|
||||
file of what you changed. The best way of preparing a patch file is
|
||||
|
||||
./touch_id newfile.bdf
|
||||
diff -d -u -F STARTCHAR oldfile.bdf newfile.bdf >file.diff
|
||||
|
||||
which ensures that the patch file preserves information about which
|
||||
exact version you worked on and what character each "hunk" changes.
|
||||
|
||||
I will try to update this packet on a daily basis. By sending me
|
||||
extensions to these fonts, you agree that the resulting improved font
|
||||
files will remain in the public domain for everyone's free use. Always
|
||||
make sure to load the very latest version of the package immediately
|
||||
before your start, and send me your results as soon as you are done,
|
||||
in order to avoid revision overlaps with other contributors.
|
||||
|
||||
Please try to be careful with the glyphs you generate:
|
||||
|
||||
- Always look first at existing similar characters in order to
|
||||
preserve a consistent look and feel for the entire font and
|
||||
within the font family. For block graphics characters and geometric
|
||||
symbols, take care of correct alignment.
|
||||
|
||||
- Read issues.txt, which contains some design hints for certain
|
||||
characters.
|
||||
|
||||
- All characters of CharCell (C) fonts must strictly fit into
|
||||
the pixel matrix and absolutely no out-of-box ink is allowed.
|
||||
|
||||
- The character cells will be displayed directly next to each other,
|
||||
without any additional pixels in between. Therefore, always make
|
||||
sure that at least the rightmost pixel column remains white, as
|
||||
otherwise letters will stick together, except of course for
|
||||
characters -- like Arabic or block graphics -- that are supposed to
|
||||
stick together.
|
||||
|
||||
- Place accents as low as possible on the Latin characters.
|
||||
|
||||
- Try to keep the shape of accents consistent among each other and
|
||||
with the combining characters in the U+03xx range.
|
||||
|
||||
- Use gbdfed only to edit the BDF file directly and do not import
|
||||
the font that you want to edit from the X server. Use gbdfed 1.3
|
||||
or higher.
|
||||
|
||||
- The glyph names should be the Adobe names for Unicode characters
|
||||
defined at
|
||||
|
||||
http://www.adobe.com/devnet/opentype/archives/glyph.html
|
||||
|
||||
which gbdfed can set automatically. To make the Edit/Rename Glyphs/
|
||||
Adobe Names function work, you have to download the file
|
||||
|
||||
http://www.adobe.com/devnet/opentype/archives/glyphlist.txt
|
||||
|
||||
and configure its location either in Edit/Preferences/Editing Options/
|
||||
Adobe Glyph List, or as "adobe_name_file" in "~/.gbdfed".
|
||||
|
||||
- Be careful to not change the FONTBOUNDINGBOX box accidentally in
|
||||
a patch.
|
||||
|
||||
You should have a copy of the ISO 10646 standard
|
||||
|
||||
ISO/IEC 10646:2003, Information technology -- Universal
|
||||
Multiple-Octet Coded Character Set (UCS),
|
||||
International Organization for Standardization, Geneva, 2003.
|
||||
http://standards.iso.org/ittf/PubliclyAvailableStandards/
|
||||
|
||||
and/or the Unicode 5.0 book:
|
||||
|
||||
The Unicode Consortium: The Unicode Standard, Version 5.0,
|
||||
Reading, MA, Addison-Wesley, 2006,
|
||||
ISBN 9780321480910.
|
||||
http://www.amazon.com/exec/obidos/ASIN/0321480910/mgk25
|
||||
|
||||
All these fonts are from time to time resubmitted to the X.Org
|
||||
project, XFree86 (they have been in there since XFree86 4.0), and to
|
||||
other X server developers for inclusion into their normal X11
|
||||
distributions.
|
||||
|
||||
Starting with XFree86 4.0, xterm has included UTF-8 support. This
|
||||
version is also available from
|
||||
|
||||
http://dickey.his.com/xterm/xterm.html
|
||||
|
||||
Please make the developer of your favourite software aware of the
|
||||
UTF-8 definition in RFC 2279 and of the existence of this font
|
||||
collection. For more information on how to use UTF-8, please check out
|
||||
|
||||
http://www.cl.cam.ac.uk/~mgk25/unicode.html
|
||||
ftp://ftp.ilog.fr/pub/Users/haible/utf8/Unicode-HOWTO.html
|
||||
|
||||
where you will also find information on joining the
|
||||
linux-utf8@nl.linux.org mailing list.
|
||||
|
||||
A number of UTF-8 example text files can be found in the examples/
|
||||
subdirectory or on
|
||||
|
||||
http://www.cl.cam.ac.uk/~mgk25/ucs/examples/
|
||||
|
||||
72
assets/fonts/README.md
Normal file
72
assets/fonts/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
## Provided fonts
|
||||
These are BDF fonts, a simple bitmap font-format that can be created
|
||||
by many font tools. Given that these are bitmap fonts, they will look good on
|
||||
very low resolution screens such as the LED displays.
|
||||
|
||||
Fonts in this directory (except tom-thumb.bdf) are public domain (see the [README](./README)) and
|
||||
help you to get started with the font support in the API or the `text-util`
|
||||
from the utils/ directory.
|
||||
|
||||
Tom-Thumb.bdf is included in this directory under [MIT license](http://vt100.tarunz.org/LICENSE). Tom-thumb.bdf was created by [@robey](http://twitter.com/robey) and originally published at https://robey.lag.net/2010/01/23/tiny-monospace-font.html
|
||||
|
||||
The texgyre-27.bdf font was created using the [otf2bdf] tool from the TeX Gyre font.
|
||||
```bash
|
||||
otf2bdf -v -o texgyre-27.bdf -r 72 -p 27 texgyreadventor-regular.otf
|
||||
```
|
||||
|
||||
## Create your own
|
||||
|
||||
Fonts are in a human-readable and editable `*.bdf` format, but unless you
|
||||
like reading and writing pixels in hex, generating them is probably easier :)
|
||||
|
||||
You can use any font-editor to generate a BDF font or use the conversion
|
||||
tool [otf2bdf] to create one from some other font format.
|
||||
|
||||
Here is an example how you could create a 30-pixel high BDF font from some
|
||||
TrueType font:
|
||||
|
||||
```bash
|
||||
otf2bdf -v -o myfont.bdf -r 72 -p 30 /path/to/font-Bold.ttf
|
||||
```
|
||||
|
||||
## Getting otf2bdf
|
||||
|
||||
Installing the tool should be fairly straightforward.
|
||||
|
||||
```bash
|
||||
sudo apt-get install otf2bdf
|
||||
```
|
||||
|
||||
## Compiling otf2bdf
|
||||
|
||||
If you like to compile otf2bdf, you might notice that the configure script
|
||||
uses some old way of getting the freetype configuration. There does not seem
|
||||
to be much activity on the mature code, so let's patch that first:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libfreetype6-dev pkg-config autoconf
|
||||
git clone https://github.com/jirutka/otf2bdf.git # check it out
|
||||
cd otf2bdf
|
||||
patch -p1 <<"EOF"
|
||||
--- a/configure.in
|
||||
+++ b/configure.in
|
||||
@@ -5,8 +5,8 @@ AC_INIT(otf2bdf.c)
|
||||
AC_PROG_CC
|
||||
|
||||
OLDLIBS=$LIBS
|
||||
-LIBS="$LIBS `freetype-config --libs`"
|
||||
-CPPFLAGS="$CPPFLAGS `freetype-config --cflags`"
|
||||
+LIBS="$LIBS `pkg-config freetype2 --libs`"
|
||||
+CPPFLAGS="$CPPFLAGS `pkg-config freetype2 --cflags`"
|
||||
AC_CHECK_LIB(freetype, FT_Init_FreeType, LIBS="$LIBS -lfreetype",[
|
||||
AC_MSG_ERROR([Can't find Freetype library! Compile FreeType first.])])
|
||||
AC_SUBST(LIBS)
|
||||
EOF
|
||||
|
||||
autoconf # rebuild configure script
|
||||
./configure # run configure
|
||||
make # build the software
|
||||
sudo make install # install it
|
||||
```
|
||||
|
||||
[otf2bdf]: https://github.com/jirutka/otf2bdf
|
||||
22736
assets/fonts/clR6x12.bdf
Normal file
22736
assets/fonts/clR6x12.bdf
Normal file
File diff suppressed because it is too large
Load Diff
32869
assets/fonts/helvR12.bdf
Normal file
32869
assets/fonts/helvR12.bdf
Normal file
File diff suppressed because it is too large
Load Diff
30577
assets/fonts/texgyre-27.bdf
Normal file
30577
assets/fonts/texgyre-27.bdf
Normal file
File diff suppressed because it is too large
Load Diff
2365
assets/fonts/tom-thumb.bdf
Normal file
2365
assets/fonts/tom-thumb.bdf
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
from flask import Blueprint, request, jsonify, Response, send_from_directory
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import subprocess
|
||||
import time
|
||||
@@ -37,6 +38,20 @@ operation_history = None
|
||||
# Get project root directory (web_interface/../..)
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
|
||||
|
||||
# System fonts that cannot be deleted (used by catalog API and delete endpoint)
|
||||
SYSTEM_FONTS = frozenset([
|
||||
'pressstart2p-regular', 'pressstart2p',
|
||||
'4x6-font', '4x6',
|
||||
'5by7.regular', '5by7', '5x7',
|
||||
'5x8', '6x9', '6x10', '6x12', '6x13', '6x13b', '6x13o',
|
||||
'7x13', '7x13b', '7x13o', '7x14', '7x14b',
|
||||
'8x13', '8x13b', '8x13o',
|
||||
'9x15', '9x15b', '9x18', '9x18b',
|
||||
'10x20',
|
||||
'matrixchunky8', 'matrixlight6', 'tom-thumb',
|
||||
'clr6x12', 'helvr12', 'texgyre-27'
|
||||
])
|
||||
|
||||
api_v3 = Blueprint('api_v3', __name__)
|
||||
|
||||
def _ensure_cache_manager():
|
||||
@@ -5388,9 +5403,31 @@ def get_fonts_catalog():
|
||||
|
||||
# Store relative path from project root
|
||||
relative_path = str(filepath.relative_to(PROJECT_ROOT))
|
||||
catalog[family_name] = {
|
||||
font_type = 'ttf' if filename.endswith('.ttf') else 'otf' if filename.endswith('.otf') else 'bdf'
|
||||
|
||||
# Generate human-readable display name from family_name
|
||||
display_name = family_name.replace('-', ' ').replace('_', ' ')
|
||||
# Add space before capital letters for camelCase names
|
||||
display_name = re.sub(r'([a-z])([A-Z])', r'\1 \2', display_name)
|
||||
# Add space before numbers that follow letters
|
||||
display_name = re.sub(r'([a-zA-Z])(\d)', r'\1 \2', display_name)
|
||||
# Clean up multiple spaces
|
||||
display_name = ' '.join(display_name.split())
|
||||
|
||||
# Use filename (without extension) as unique key to avoid collisions
|
||||
# when multiple files share the same family_name from font metadata
|
||||
catalog_key = os.path.splitext(filename)[0]
|
||||
|
||||
# Check if this is a system font (cannot be deleted)
|
||||
is_system = catalog_key.lower() in SYSTEM_FONTS
|
||||
|
||||
catalog[catalog_key] = {
|
||||
'filename': filename,
|
||||
'family_name': family_name,
|
||||
'display_name': display_name,
|
||||
'path': relative_path,
|
||||
'type': 'ttf' if filename.endswith('.ttf') else 'otf' if filename.endswith('.otf') else 'bdf',
|
||||
'type': font_type,
|
||||
'is_system': is_system,
|
||||
'metadata': metadata if metadata else None
|
||||
}
|
||||
|
||||
@@ -5399,7 +5436,7 @@ def get_fonts_catalog():
|
||||
try:
|
||||
set_cached('fonts_catalog', catalog, ttl_seconds=300)
|
||||
except Exception:
|
||||
pass # Cache write failed, but continue
|
||||
logger.error("[FontCatalog] Failed to cache fonts_catalog", exc_info=True)
|
||||
|
||||
return jsonify({'status': 'success', 'data': {'catalog': catalog}})
|
||||
except Exception as e:
|
||||
@@ -5476,28 +5513,282 @@ def upload_font():
|
||||
if not is_valid:
|
||||
return jsonify({'status': 'error', 'message': error_msg}), 400
|
||||
|
||||
font_file = request.files['font_file']
|
||||
font_family = request.form.get('font_family', '')
|
||||
|
||||
if not font_file or not font_family:
|
||||
if not font_family:
|
||||
return jsonify({'status': 'error', 'message': 'Font file and family name required'}), 400
|
||||
|
||||
# Validate file type
|
||||
allowed_extensions = ['.ttf', '.bdf']
|
||||
file_extension = font_file.filename.lower().split('.')[-1]
|
||||
if f'.{file_extension}' not in allowed_extensions:
|
||||
return jsonify({'status': 'error', 'message': 'Only .ttf and .bdf files are allowed'}), 400
|
||||
|
||||
# Validate font family name
|
||||
if not font_family.replace('_', '').replace('-', '').isalnum():
|
||||
return jsonify({'status': 'error', 'message': 'Font family name must contain only letters, numbers, underscores, and hyphens'}), 400
|
||||
|
||||
# This would integrate with the actual font system to save the file
|
||||
# For now, just return success
|
||||
return jsonify({'status': 'success', 'message': f'Font {font_family} uploaded successfully', 'font_family': font_family})
|
||||
# Save the font file to assets/fonts directory
|
||||
fonts_dir = PROJECT_ROOT / "assets" / "fonts"
|
||||
fonts_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create filename from family name
|
||||
original_ext = os.path.splitext(font_file.filename)[1].lower()
|
||||
safe_filename = f"{font_family}{original_ext}"
|
||||
filepath = fonts_dir / safe_filename
|
||||
|
||||
# Check if file already exists
|
||||
if filepath.exists():
|
||||
return jsonify({'status': 'error', 'message': f'Font with name {font_family} already exists'}), 400
|
||||
|
||||
# Save the file
|
||||
font_file.save(str(filepath))
|
||||
|
||||
# Clear font catalog cache
|
||||
try:
|
||||
from web_interface.cache import delete_cached
|
||||
delete_cached('fonts_catalog')
|
||||
except ImportError as e:
|
||||
logger.warning("[FontUpload] Cache module not available: %s", e)
|
||||
except Exception:
|
||||
logger.error("[FontUpload] Failed to clear fonts_catalog cache", exc_info=True)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Font {font_family} uploaded successfully',
|
||||
'font_family': font_family,
|
||||
'filename': safe_filename,
|
||||
'path': f'assets/fonts/{safe_filename}'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/fonts/preview', methods=['GET'])
|
||||
def get_font_preview() -> tuple[Response, int] | Response:
|
||||
"""Generate a preview image of text rendered with a specific font"""
|
||||
try:
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import io
|
||||
import base64
|
||||
|
||||
# Limits to prevent DoS via large image generation on constrained devices
|
||||
MAX_TEXT_CHARS = 100
|
||||
MAX_TEXT_LINES = 3
|
||||
MAX_DIM = 1024 # Max width or height in pixels
|
||||
MAX_PIXELS = 500000 # Max total pixels (e.g., ~700x700)
|
||||
|
||||
font_filename = request.args.get('font', '')
|
||||
text = request.args.get('text', 'Sample Text 123')
|
||||
bg_color = request.args.get('bg', '000000')
|
||||
fg_color = request.args.get('fg', 'ffffff')
|
||||
|
||||
# Validate text length and line count early
|
||||
if len(text) > MAX_TEXT_CHARS:
|
||||
return jsonify({'status': 'error', 'message': f'Text exceeds maximum length of {MAX_TEXT_CHARS} characters'}), 400
|
||||
if text.count('\n') >= MAX_TEXT_LINES:
|
||||
return jsonify({'status': 'error', 'message': f'Text exceeds maximum of {MAX_TEXT_LINES} lines'}), 400
|
||||
|
||||
# Safe integer parsing for size
|
||||
try:
|
||||
size = int(request.args.get('size', 12))
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid font size'}), 400
|
||||
|
||||
if not font_filename:
|
||||
return jsonify({'status': 'error', 'message': 'Font filename required'}), 400
|
||||
|
||||
# Validate size
|
||||
if size < 4 or size > 72:
|
||||
return jsonify({'status': 'error', 'message': 'Font size must be between 4 and 72'}), 400
|
||||
|
||||
# Security: Validate font_filename to prevent path traversal
|
||||
# Only allow alphanumeric, hyphen, underscore, and dot (for extension)
|
||||
safe_name = Path(font_filename).name # Strip any directory components
|
||||
if safe_name != font_filename or '..' in font_filename:
|
||||
return jsonify({'status': 'error', 'message': 'Invalid font filename'}), 400
|
||||
|
||||
# Validate extension
|
||||
allowed_extensions = ['.ttf', '.otf', '.bdf']
|
||||
has_valid_ext = any(safe_name.lower().endswith(ext) for ext in allowed_extensions)
|
||||
name_without_ext = safe_name.rsplit('.', 1)[0] if '.' in safe_name else safe_name
|
||||
|
||||
# Find the font file
|
||||
fonts_dir = PROJECT_ROOT / "assets" / "fonts"
|
||||
if not fonts_dir.exists():
|
||||
return jsonify({'status': 'error', 'message': 'Fonts directory not found'}), 404
|
||||
|
||||
font_path = fonts_dir / safe_name
|
||||
|
||||
if not font_path.exists() and not has_valid_ext:
|
||||
# Try finding by family name (without extension)
|
||||
for ext in allowed_extensions:
|
||||
potential_path = fonts_dir / f"{name_without_ext}{ext}"
|
||||
if potential_path.exists():
|
||||
font_path = potential_path
|
||||
break
|
||||
|
||||
# Final security check: ensure path is within fonts_dir
|
||||
try:
|
||||
font_path.resolve().relative_to(fonts_dir.resolve())
|
||||
except ValueError:
|
||||
return jsonify({'status': 'error', 'message': 'Invalid font path'}), 400
|
||||
|
||||
if not font_path.exists():
|
||||
return jsonify({'status': 'error', 'message': f'Font file not found: {font_filename}'}), 404
|
||||
|
||||
# Parse colors
|
||||
try:
|
||||
bg_rgb = tuple(int(bg_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
fg_rgb = tuple(int(fg_color[i:i+2], 16) for i in (0, 2, 4))
|
||||
except (ValueError, IndexError):
|
||||
bg_rgb = (0, 0, 0)
|
||||
fg_rgb = (255, 255, 255)
|
||||
|
||||
# Load font
|
||||
font = None
|
||||
if str(font_path).endswith('.bdf'):
|
||||
# BDF fonts require complex per-glyph rendering via freetype
|
||||
# Return explicit error rather than showing misleading preview with default font
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': 'BDF font preview not supported. BDF fonts will render correctly on the LED matrix.'
|
||||
}), 400
|
||||
else:
|
||||
# TTF/OTF fonts
|
||||
try:
|
||||
font = ImageFont.truetype(str(font_path), size)
|
||||
except (IOError, OSError) as e:
|
||||
# IOError/OSError raised for invalid/corrupt font files
|
||||
logger.warning("[FontPreview] Failed to load font %s: %s", font_path, e)
|
||||
font = ImageFont.load_default()
|
||||
|
||||
# Calculate text size
|
||||
temp_img = Image.new('RGB', (1, 1))
|
||||
temp_draw = ImageDraw.Draw(temp_img)
|
||||
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
||||
text_width = bbox[2] - bbox[0]
|
||||
text_height = bbox[3] - bbox[1]
|
||||
|
||||
# Create image with padding
|
||||
padding = 10
|
||||
img_width = max(text_width + padding * 2, 100)
|
||||
img_height = max(text_height + padding * 2, 30)
|
||||
|
||||
# Validate resulting image size to prevent memory/CPU spikes
|
||||
if img_width > MAX_DIM or img_height > MAX_DIM:
|
||||
return jsonify({'status': 'error', 'message': 'Requested image too large'}), 400
|
||||
if img_width * img_height > MAX_PIXELS:
|
||||
return jsonify({'status': 'error', 'message': 'Requested image too large'}), 400
|
||||
|
||||
img = Image.new('RGB', (img_width, img_height), bg_rgb)
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
# Center text
|
||||
x = (img_width - text_width) // 2
|
||||
y = (img_height - text_height) // 2
|
||||
|
||||
draw.text((x, y), text, font=font, fill=fg_rgb)
|
||||
|
||||
# Convert to base64
|
||||
buffer = io.BytesIO()
|
||||
img.save(buffer, format='PNG')
|
||||
buffer.seek(0)
|
||||
img_base64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'image': f'data:image/png;base64,{img_base64}',
|
||||
'width': img_width,
|
||||
'height': img_height
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/fonts/<font_family>', methods=['DELETE'])
|
||||
def delete_font(font_family: str) -> tuple[Response, int] | Response:
|
||||
"""Delete a user-uploaded font file"""
|
||||
try:
|
||||
# Security: Validate font_family to prevent path traversal
|
||||
# Reject if it contains path separators or ..
|
||||
if '..' in font_family or '/' in font_family or '\\' in font_family:
|
||||
return jsonify({'status': 'error', 'message': 'Invalid font family name'}), 400
|
||||
|
||||
# Only allow safe characters: alphanumeric, hyphen, underscore, dot
|
||||
if not re.match(r'^[a-zA-Z0-9_\-\.]+$', font_family):
|
||||
return jsonify({'status': 'error', 'message': 'Invalid font family name'}), 400
|
||||
|
||||
# Check if this is a system font (uses module-level SYSTEM_FONTS frozenset)
|
||||
if font_family.lower() in SYSTEM_FONTS:
|
||||
return jsonify({'status': 'error', 'message': 'Cannot delete system fonts'}), 403
|
||||
|
||||
# Find and delete the font file
|
||||
fonts_dir = PROJECT_ROOT / "assets" / "fonts"
|
||||
|
||||
# Ensure fonts directory exists
|
||||
if not fonts_dir.exists() or not fonts_dir.is_dir():
|
||||
return jsonify({'status': 'error', 'message': 'Fonts directory not found'}), 404
|
||||
|
||||
deleted = False
|
||||
deleted_filename = None
|
||||
|
||||
# Only try valid font extensions (no empty string to avoid matching directories)
|
||||
for ext in ['.ttf', '.otf', '.bdf']:
|
||||
potential_path = fonts_dir / f"{font_family}{ext}"
|
||||
|
||||
# Security: Verify path is within fonts_dir
|
||||
try:
|
||||
potential_path.resolve().relative_to(fonts_dir.resolve())
|
||||
except ValueError:
|
||||
continue # Path escapes fonts_dir, skip
|
||||
|
||||
if potential_path.exists() and potential_path.is_file():
|
||||
potential_path.unlink()
|
||||
deleted = True
|
||||
deleted_filename = f"{font_family}{ext}"
|
||||
break
|
||||
|
||||
if not deleted:
|
||||
# Try case-insensitive match within fonts directory
|
||||
font_family_lower = font_family.lower()
|
||||
for filename in os.listdir(fonts_dir):
|
||||
# Only consider files with valid font extensions
|
||||
if not any(filename.lower().endswith(ext) for ext in ['.ttf', '.otf', '.bdf']):
|
||||
continue
|
||||
|
||||
name_without_ext = os.path.splitext(filename)[0]
|
||||
if name_without_ext.lower() == font_family_lower:
|
||||
filepath = fonts_dir / filename
|
||||
|
||||
# Security: Verify path is within fonts_dir
|
||||
try:
|
||||
filepath.resolve().relative_to(fonts_dir.resolve())
|
||||
except ValueError:
|
||||
continue # Path escapes fonts_dir, skip
|
||||
|
||||
if filepath.is_file():
|
||||
filepath.unlink()
|
||||
deleted = True
|
||||
deleted_filename = filename
|
||||
break
|
||||
|
||||
if not deleted:
|
||||
return jsonify({'status': 'error', 'message': f'Font not found: {font_family}'}), 404
|
||||
|
||||
# Clear font catalog cache
|
||||
try:
|
||||
from web_interface.cache import delete_cached
|
||||
delete_cached('fonts_catalog')
|
||||
except ImportError as e:
|
||||
logger.warning("[FontDelete] Cache module not available: %s", e)
|
||||
except Exception:
|
||||
logger.error("[FontDelete] Failed to clear fonts_catalog cache", exc_info=True)
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': f'Font {deleted_filename} deleted successfully'
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
|
||||
@api_v3.route('/plugins/assets/upload', methods=['POST'])
|
||||
def upload_plugin_asset():
|
||||
"""Upload asset files for a plugin"""
|
||||
@@ -6044,15 +6335,6 @@ def list_plugin_assets():
|
||||
import traceback
|
||||
return jsonify({'status': 'error', 'message': str(e), 'traceback': traceback.format_exc()}), 500
|
||||
|
||||
@api_v3.route('/fonts/delete/<font_family>', methods=['DELETE'])
|
||||
def delete_font(font_family):
|
||||
"""Delete font"""
|
||||
try:
|
||||
# This would integrate with the actual font system
|
||||
return jsonify({'status': 'success', 'message': f'Font {font_family} deleted'})
|
||||
except Exception as e:
|
||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||||
|
||||
@api_v3.route('/logs', methods=['GET'])
|
||||
def get_logs():
|
||||
"""Get system logs from journalctl"""
|
||||
|
||||
298
web_interface/static/v3/js/widgets/font-selector.js
Normal file
298
web_interface/static/v3/js/widgets/font-selector.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/**
|
||||
* LEDMatrix Font Selector Widget
|
||||
*
|
||||
* Dynamic font selector that fetches available fonts from the API.
|
||||
* Automatically shows all fonts in assets/fonts/ directory.
|
||||
*
|
||||
* Schema example:
|
||||
* {
|
||||
* "font": {
|
||||
* "type": "string",
|
||||
* "title": "Font Family",
|
||||
* "x-widget": "font-selector",
|
||||
* "x-options": {
|
||||
* "placeholder": "Select a font...",
|
||||
* "showPreview": false,
|
||||
* "filterTypes": ["ttf", "bdf"]
|
||||
* },
|
||||
* "default": "PressStart2P-Regular.ttf"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @module FontSelectorWidget
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const base = window.BaseWidget ? new window.BaseWidget('FontSelector', '1.0.0') : null;
|
||||
|
||||
// Cache for font catalog to avoid repeated API calls
|
||||
let fontCatalogCache = null;
|
||||
let fontCatalogPromise = null;
|
||||
|
||||
function escapeHtml(text) {
|
||||
if (base) return base.escapeHtml(text);
|
||||
const div = document.createElement('div');
|
||||
div.textContent = String(text);
|
||||
return div.innerHTML.replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function sanitizeId(id) {
|
||||
if (base) return base.sanitizeId(id);
|
||||
return String(id).replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||
}
|
||||
|
||||
function triggerChange(fieldId, value) {
|
||||
if (base) {
|
||||
base.triggerChange(fieldId, value);
|
||||
} else {
|
||||
const event = new CustomEvent('widget-change', {
|
||||
detail: { fieldId, value },
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable display name from font filename
|
||||
* @param {string} filename - Font filename (e.g., "PressStart2P-Regular.ttf")
|
||||
* @returns {string} Display name (e.g., "Press Start 2P Regular")
|
||||
*/
|
||||
function generateDisplayName(filename) {
|
||||
if (!filename) return '';
|
||||
|
||||
// Remove extension
|
||||
let name = filename.replace(/\.(ttf|bdf|otf)$/i, '');
|
||||
|
||||
// Handle common patterns
|
||||
// Split on hyphens and underscores
|
||||
name = name.replace(/[-_]/g, ' ');
|
||||
|
||||
// Add space before capital letters (camelCase/PascalCase)
|
||||
name = name.replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||
|
||||
// Add space before numbers that follow letters
|
||||
name = name.replace(/([a-zA-Z])(\d)/g, '$1 $2');
|
||||
|
||||
// Clean up multiple spaces
|
||||
name = name.replace(/\s+/g, ' ').trim();
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch font catalog from API (with caching)
|
||||
* @returns {Promise<Array>} Array of font objects
|
||||
*/
|
||||
async function fetchFontCatalog() {
|
||||
// Return cached data if available
|
||||
if (fontCatalogCache) {
|
||||
return fontCatalogCache;
|
||||
}
|
||||
|
||||
// Return existing promise if fetch is in progress
|
||||
if (fontCatalogPromise) {
|
||||
return fontCatalogPromise;
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
fontCatalogPromise = fetch('/api/v3/fonts/catalog')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch font catalog: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
// Handle different response structures
|
||||
let fonts = [];
|
||||
|
||||
if (data.data && data.data.fonts) {
|
||||
// New format: { data: { fonts: [...] } }
|
||||
fonts = data.data.fonts;
|
||||
} else if (data.data && data.data.catalog) {
|
||||
// Alternative format: { data: { catalog: {...} } }
|
||||
const catalog = data.data.catalog;
|
||||
fonts = Object.entries(catalog).map(([family, info]) => ({
|
||||
filename: info.filename || family,
|
||||
family: family,
|
||||
display_name: info.display_name || generateDisplayName(info.filename || family),
|
||||
path: info.path,
|
||||
type: info.type || 'unknown'
|
||||
}));
|
||||
} else if (Array.isArray(data)) {
|
||||
// Direct array format
|
||||
fonts = data;
|
||||
}
|
||||
|
||||
// Sort fonts alphabetically by display name
|
||||
fonts.sort((a, b) => {
|
||||
const nameA = (a.display_name || a.filename || '').toLowerCase();
|
||||
const nameB = (b.display_name || b.filename || '').toLowerCase();
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
|
||||
fontCatalogCache = fonts;
|
||||
fontCatalogPromise = null;
|
||||
return fonts;
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[FontSelectorWidget] Error fetching font catalog:', error);
|
||||
fontCatalogPromise = null;
|
||||
return [];
|
||||
});
|
||||
|
||||
return fontCatalogPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the font catalog cache (call when fonts are uploaded/deleted)
|
||||
*/
|
||||
function clearFontCache() {
|
||||
fontCatalogCache = null;
|
||||
fontCatalogPromise = null;
|
||||
}
|
||||
|
||||
// Expose cache clearing function globally
|
||||
window.clearFontSelectorCache = clearFontCache;
|
||||
|
||||
// Guard against missing global registry
|
||||
if (!window.LEDMatrixWidgets || typeof window.LEDMatrixWidgets.register !== 'function') {
|
||||
console.error('[FontSelectorWidget] LEDMatrixWidgets registry not available');
|
||||
return;
|
||||
}
|
||||
|
||||
window.LEDMatrixWidgets.register('font-selector', {
|
||||
name: 'Font Selector Widget',
|
||||
version: '1.0.0',
|
||||
|
||||
render: async function(container, config, value, options) {
|
||||
const fieldId = sanitizeId(options.fieldId || container.id || 'font-select');
|
||||
const xOptions = config['x-options'] || config['x_options'] || {};
|
||||
const placeholder = xOptions.placeholder || 'Select a font...';
|
||||
const filterTypes = xOptions.filterTypes || null; // e.g., ['ttf', 'bdf']
|
||||
const showPreview = xOptions.showPreview === true;
|
||||
const disabled = xOptions.disabled === true;
|
||||
const required = xOptions.required === true;
|
||||
|
||||
const currentValue = value !== null && value !== undefined ? String(value) : '';
|
||||
|
||||
// Show loading state
|
||||
container.innerHTML = `
|
||||
<div id="${fieldId}_widget" class="font-selector-widget" data-field-id="${fieldId}">
|
||||
<select id="${fieldId}_input"
|
||||
name="${escapeHtml(options.name || fieldId)}"
|
||||
disabled
|
||||
class="form-select w-full rounded-md border-gray-300 shadow-sm bg-gray-100 text-black">
|
||||
<option value="">Loading fonts...</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
// Fetch fonts from API
|
||||
const fonts = await fetchFontCatalog();
|
||||
|
||||
// Filter by type if specified
|
||||
let filteredFonts = fonts;
|
||||
if (filterTypes && Array.isArray(filterTypes)) {
|
||||
filteredFonts = fonts.filter(font => {
|
||||
const fontType = (font.type || '').toLowerCase();
|
||||
return filterTypes.some(t => t.toLowerCase() === fontType);
|
||||
});
|
||||
}
|
||||
|
||||
// Build select HTML
|
||||
let html = `<div id="${fieldId}_widget" class="font-selector-widget" data-field-id="${fieldId}">`;
|
||||
|
||||
html += `
|
||||
<select id="${fieldId}_input"
|
||||
name="${escapeHtml(options.name || fieldId)}"
|
||||
${disabled ? 'disabled' : ''}
|
||||
${required ? 'required' : ''}
|
||||
onchange="window.LEDMatrixWidgets.getHandlers('font-selector').onChange('${fieldId}')"
|
||||
class="form-select w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 ${disabled ? 'bg-gray-100 cursor-not-allowed' : 'bg-white'} text-black">
|
||||
`;
|
||||
|
||||
// Placeholder option
|
||||
if (placeholder && !required) {
|
||||
html += `<option value="" ${!currentValue ? 'selected' : ''}>${escapeHtml(placeholder)}</option>`;
|
||||
}
|
||||
|
||||
// Font options
|
||||
for (const font of filteredFonts) {
|
||||
const fontValue = font.filename || font.family;
|
||||
const displayName = font.display_name || generateDisplayName(fontValue);
|
||||
const fontType = font.type ? ` (${font.type.toUpperCase()})` : '';
|
||||
const isSelected = String(fontValue) === currentValue;
|
||||
|
||||
html += `<option value="${escapeHtml(String(fontValue))}" ${isSelected ? 'selected' : ''}>${escapeHtml(displayName)}${escapeHtml(fontType)}</option>`;
|
||||
}
|
||||
|
||||
html += '</select>';
|
||||
|
||||
// Optional preview area
|
||||
if (showPreview) {
|
||||
html += `
|
||||
<div id="${fieldId}_preview" class="mt-2 p-2 bg-gray-800 rounded text-white text-center" style="min-height: 30px;">
|
||||
<span style="font-family: monospace;">Preview</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Error message area
|
||||
html += `<div id="${fieldId}_error" class="text-sm text-red-600 mt-1 hidden"></div>`;
|
||||
|
||||
html += '</div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[FontSelectorWidget] Error rendering:', error);
|
||||
container.innerHTML = `
|
||||
<div id="${fieldId}_widget" class="font-selector-widget" data-field-id="${fieldId}">
|
||||
<select id="${fieldId}_input"
|
||||
name="${escapeHtml(options.name || fieldId)}"
|
||||
class="form-select w-full rounded-md border-gray-300 shadow-sm bg-white text-black">
|
||||
<option value="${escapeHtml(currentValue)}" selected>${escapeHtml(currentValue || 'Error loading fonts')}</option>
|
||||
</select>
|
||||
<div class="text-sm text-red-600 mt-1">Failed to load font list</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
},
|
||||
|
||||
getValue: function(fieldId) {
|
||||
const safeId = sanitizeId(fieldId);
|
||||
const input = document.getElementById(`${safeId}_input`);
|
||||
return input ? input.value : '';
|
||||
},
|
||||
|
||||
setValue: function(fieldId, value) {
|
||||
const safeId = sanitizeId(fieldId);
|
||||
const input = document.getElementById(`${safeId}_input`);
|
||||
if (input) {
|
||||
input.value = value !== null && value !== undefined ? String(value) : '';
|
||||
}
|
||||
},
|
||||
|
||||
handlers: {
|
||||
onChange: function(fieldId) {
|
||||
const widget = window.LEDMatrixWidgets.get('font-selector');
|
||||
triggerChange(fieldId, widget.getValue(fieldId));
|
||||
}
|
||||
},
|
||||
|
||||
// Expose utility functions
|
||||
utils: {
|
||||
clearCache: clearFontCache,
|
||||
fetchCatalog: fetchFontCatalog,
|
||||
generateDisplayName: generateDisplayName
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[FontSelectorWidget] Font selector widget registered');
|
||||
})();
|
||||
@@ -4908,6 +4908,7 @@
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/number-input.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/textarea.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/select-dropdown.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/font-selector.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/toggle-switch.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/radio-group.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='v3/js/widgets/date-picker.js') }}" defer></script>
|
||||
|
||||
@@ -28,14 +28,14 @@
|
||||
<!-- Font Upload -->
|
||||
<div class="bg-gray-50 rounded-lg p-4 mb-8">
|
||||
<h3 class="text-md font-medium text-gray-900 mb-4">Upload Custom Fonts</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Upload your own TTF or BDF font files to use in your LED matrix display.</p>
|
||||
<p class="text-sm text-gray-600 mb-4">Upload your own TTF, OTF, or BDF font files to use in your LED matrix display.</p>
|
||||
|
||||
<div class="font-upload-area" id="font-upload-area">
|
||||
<div class="upload-dropzone" id="upload-dropzone">
|
||||
<i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-3"></i>
|
||||
<p class="text-gray-600">Drag and drop font files here, or click to select</p>
|
||||
<p class="text-sm text-gray-500">Supports .ttf and .bdf files</p>
|
||||
<input type="file" id="font-file-input" accept=".ttf,.bdf" multiple style="display: none;">
|
||||
<p class="text-sm text-gray-500">Supports .ttf, .otf, and .bdf files</p>
|
||||
<input type="file" id="font-file-input" accept=".ttf,.otf,.bdf" multiple style="display: none;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,8 +49,8 @@
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Font Family Name</label>
|
||||
<input type="text" id="upload-font-family" class="form-control" placeholder="e.g., my_custom_font">
|
||||
<p class="text-sm text-gray-600 mt-1">Custom name for this font (letters, numbers, underscores only)</p>
|
||||
<input type="text" id="upload-font-family" class="form-control" placeholder="e.g., my-custom-font">
|
||||
<p class="text-sm text-gray-600 mt-1">Custom name for this font (letters, numbers, underscores, hyphens)</p>
|
||||
</div>
|
||||
<div class="flex items-end">
|
||||
<button type="button" id="upload-fonts-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2">
|
||||
@@ -112,9 +112,7 @@
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Font Family</label>
|
||||
<select id="override-family" class="form-control text-sm">
|
||||
<option value="">Use default</option>
|
||||
<option value="press_start">Press Start 2P</option>
|
||||
<option value="four_by_six">4x6 Font</option>
|
||||
<option value="matrix_light_6">Matrix Light 6</option>
|
||||
<!-- Dynamically populated from font catalog -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +150,10 @@
|
||||
<h3 class="text-md font-medium text-gray-900 mb-4">Font Preview</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<canvas id="font-preview-canvas" width="400" height="100" class="border border-gray-300 bg-black rounded"></canvas>
|
||||
<div id="font-preview-container" class="border border-gray-300 bg-black rounded p-4 min-h-[100px] flex items-center justify-center">
|
||||
<img id="font-preview-image" src="" alt="Font preview" class="max-w-full" style="display: none;">
|
||||
<span id="font-preview-loading" class="text-gray-400">Select a font to preview</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
@@ -163,9 +164,7 @@
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Font Family</label>
|
||||
<select id="preview-family" class="form-control text-sm">
|
||||
<option value="press_start">Press Start 2P</option>
|
||||
<option value="four_by_six">4x6 Font</option>
|
||||
<option value="matrix_light_6">Matrix Light 6</option>
|
||||
<!-- Dynamically populated from font catalog -->
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -209,27 +208,43 @@
|
||||
window.fontTokens = window.fontTokens || {};
|
||||
window.fontOverrides = window.fontOverrides || {};
|
||||
window.selectedFontFiles = window.selectedFontFiles || [];
|
||||
|
||||
|
||||
// Create references that can be reassigned
|
||||
var fontCatalog = window.fontCatalog;
|
||||
var fontTokens = window.fontTokens;
|
||||
var fontOverrides = window.fontOverrides;
|
||||
var selectedFontFiles = window.selectedFontFiles;
|
||||
|
||||
// Base URL for API calls (shared scope)
|
||||
var baseUrl = window.location.origin;
|
||||
|
||||
// Retry counter for initialization
|
||||
var initRetryCount = 0;
|
||||
var MAX_INIT_RETRIES = 50; // 5 seconds max (50 * 100ms)
|
||||
|
||||
function initializeFontsTab() {
|
||||
// Allow re-initialization on each HTMX content swap
|
||||
// The window._fontsScriptLoaded guard prevents function redeclaration
|
||||
const detectedEl = document.getElementById('detected-fonts');
|
||||
const availableEl = document.getElementById('available-fonts');
|
||||
|
||||
|
||||
if (!detectedEl || !availableEl) {
|
||||
initRetryCount++;
|
||||
if (initRetryCount >= MAX_INIT_RETRIES) {
|
||||
console.error('Fonts tab elements not found after max retries, giving up');
|
||||
return;
|
||||
}
|
||||
console.log('Fonts tab elements not found, retrying...', {
|
||||
detectedFonts: !!detectedEl,
|
||||
availableFonts: !!availableEl
|
||||
availableFonts: !!availableEl,
|
||||
attempt: initRetryCount
|
||||
});
|
||||
setTimeout(initializeFontsTab, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset retry counter on successful init
|
||||
initRetryCount = 0;
|
||||
|
||||
// showNotification is provided by the notification widget (notification.js)
|
||||
// Fallback only if widget hasn't loaded yet
|
||||
@@ -368,7 +383,6 @@ async function loadFontData() {
|
||||
|
||||
try {
|
||||
// Use absolute URLs to ensure they work when loaded via HTMX
|
||||
const baseUrl = window.location.origin;
|
||||
const [catalogRes, tokensRes, overridesRes] = await Promise.all([
|
||||
fetch(`${baseUrl}/api/v3/fonts/catalog`),
|
||||
fetch(`${baseUrl}/api/v3/fonts/tokens`),
|
||||
@@ -488,21 +502,146 @@ function updateAvailableFontsDisplay() {
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = Object.entries(fontCatalog).map(([name, fontInfo]) => {
|
||||
const fontEntries = Object.entries(fontCatalog).map(([name, fontInfo]) => {
|
||||
const fontPath = typeof fontInfo === 'string' ? fontInfo : (fontInfo?.path || '');
|
||||
// Only prefix with "assets/fonts/" if path is a bare filename (no "/" and doesn't start with "assets/")
|
||||
// If path is absolute (starts with "/") or already has "assets/" prefix, use as-is
|
||||
const fullPath = (fontPath.startsWith('/') || fontPath.startsWith('assets/'))
|
||||
? fontPath
|
||||
: `assets/fonts/${fontPath}`;
|
||||
return `${name}: ${fullPath}`;
|
||||
const filename = typeof fontInfo === 'object' ? (fontInfo.filename || name) : name;
|
||||
const displayName = typeof fontInfo === 'object' ? (fontInfo.display_name || name) : name;
|
||||
const fontType = typeof fontInfo === 'object' ? (fontInfo.type || '').toUpperCase() : '';
|
||||
// Use is_system flag from API (single source of truth)
|
||||
const isSystem = typeof fontInfo === 'object' ? (fontInfo.is_system === true) : false;
|
||||
return { name, filename, displayName, fontType, fontPath, isSystem };
|
||||
}).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
|
||||
// Build list using DOM APIs to prevent XSS
|
||||
container.innerHTML = '';
|
||||
fontEntries.forEach(font => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center justify-between py-1 border-b border-gray-700 last:border-0';
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
nameSpan.className = 'truncate flex-1';
|
||||
nameSpan.textContent = font.displayName;
|
||||
|
||||
if (font.fontType) {
|
||||
const typeSpan = document.createElement('span');
|
||||
typeSpan.className = 'text-gray-500 ml-1';
|
||||
typeSpan.textContent = `(${font.fontType})`;
|
||||
nameSpan.appendChild(typeSpan);
|
||||
}
|
||||
|
||||
row.appendChild(nameSpan);
|
||||
|
||||
if (font.isSystem) {
|
||||
const systemBadge = document.createElement('span');
|
||||
systemBadge.className = 'text-gray-600 text-xs ml-2';
|
||||
systemBadge.textContent = '[system]';
|
||||
row.appendChild(systemBadge);
|
||||
} else {
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'text-red-400 hover:text-red-300 text-xs ml-2';
|
||||
deleteBtn.title = 'Delete font';
|
||||
deleteBtn.textContent = '[delete]';
|
||||
deleteBtn.dataset.fontName = font.name;
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
deleteFont(this.dataset.fontName);
|
||||
});
|
||||
row.appendChild(deleteBtn);
|
||||
}
|
||||
|
||||
container.appendChild(row);
|
||||
});
|
||||
container.textContent = lines.join('\n');
|
||||
}
|
||||
|
||||
async function deleteFont(fontFamily) {
|
||||
if (!confirm(`Are you sure you want to delete the font "${fontFamily}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${baseUrl}/api/v3/fonts/${encodeURIComponent(fontFamily)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message;
|
||||
try {
|
||||
const errorData = JSON.parse(text);
|
||||
message = errorData.message || `Server error: ${response.status}`;
|
||||
} catch {
|
||||
message = `Server error: ${response.status}`;
|
||||
}
|
||||
showNotification(message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
showNotification(data.message || `Font "${fontFamily}" deleted successfully`, 'success');
|
||||
// Refresh font data and UI
|
||||
await loadFontData();
|
||||
populateFontSelects();
|
||||
// Clear font-selector widget cache if available
|
||||
if (typeof window.clearFontSelectorCache === 'function') {
|
||||
window.clearFontSelectorCache();
|
||||
}
|
||||
} else {
|
||||
showNotification(data.message || `Failed to delete font "${fontFamily}"`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting font:', error);
|
||||
showNotification(`Error deleting font: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function populateFontSelects() {
|
||||
// This would populate the select options with actual font data
|
||||
// For now, using placeholder options
|
||||
// Populate font family dropdowns from catalog
|
||||
const overrideSelect = document.getElementById('override-family');
|
||||
const previewSelect = document.getElementById('preview-family');
|
||||
|
||||
if (!overrideSelect || !previewSelect) return;
|
||||
|
||||
// Get font entries sorted by display name
|
||||
const fontEntries = Object.entries(fontCatalog).map(([key, info]) => {
|
||||
const filename = typeof info === 'object' ? (info.filename || key) : key;
|
||||
const displayName = typeof info === 'object' ? (info.display_name || key) : key;
|
||||
const fontType = typeof info === 'object' ? (info.type || 'unknown').toUpperCase() : '';
|
||||
return { key, filename, displayName, fontType };
|
||||
}).sort((a, b) => a.displayName.localeCompare(b.displayName));
|
||||
|
||||
// Build options using DOM APIs to prevent XSS
|
||||
// Clear and add default option for override select
|
||||
overrideSelect.innerHTML = '';
|
||||
const defaultOption = document.createElement('option');
|
||||
defaultOption.value = '';
|
||||
defaultOption.textContent = 'Use default';
|
||||
overrideSelect.appendChild(defaultOption);
|
||||
|
||||
// Clear preview select
|
||||
previewSelect.innerHTML = '';
|
||||
|
||||
// Add font options to both selects
|
||||
fontEntries.forEach(font => {
|
||||
const typeLabel = font.fontType ? ` (${font.fontType})` : '';
|
||||
|
||||
const overrideOpt = document.createElement('option');
|
||||
overrideOpt.value = font.filename;
|
||||
overrideOpt.textContent = font.displayName + typeLabel;
|
||||
overrideSelect.appendChild(overrideOpt);
|
||||
|
||||
const previewOpt = document.createElement('option');
|
||||
previewOpt.value = font.filename;
|
||||
previewOpt.textContent = font.displayName + typeLabel;
|
||||
previewSelect.appendChild(previewOpt);
|
||||
});
|
||||
|
||||
// Select first font in preview if available
|
||||
if (fontEntries.length > 0) {
|
||||
previewSelect.value = fontEntries[0].filename;
|
||||
}
|
||||
|
||||
console.log(`Populated font selects with ${fontEntries.length} fonts`);
|
||||
}
|
||||
|
||||
async function addFontOverride() {
|
||||
@@ -536,6 +675,19 @@ async function addFontOverride() {
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message;
|
||||
try {
|
||||
const errorData = JSON.parse(text);
|
||||
message = errorData.message || `Server error: ${response.status}`;
|
||||
} catch {
|
||||
message = `Server error: ${response.status}`;
|
||||
}
|
||||
showNotification('Error adding font override: ' + message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
showNotification('Font override added successfully', 'success');
|
||||
@@ -564,6 +716,19 @@ async function deleteFontOverride(elementKey) {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message;
|
||||
try {
|
||||
const errorData = JSON.parse(text);
|
||||
message = errorData.message || `Server error: ${response.status}`;
|
||||
} catch {
|
||||
message = `Server error: ${response.status}`;
|
||||
}
|
||||
showNotification('Error removing font override: ' + message, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
showNotification('Font override removed successfully', 'success');
|
||||
@@ -587,7 +752,9 @@ function displayCurrentOverrides() {
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = Object.entries(fontOverrides).map(([elementKey, override]) => {
|
||||
// Build list using DOM APIs to prevent XSS
|
||||
container.innerHTML = '';
|
||||
Object.entries(fontOverrides).forEach(([elementKey, override]) => {
|
||||
const elementName = getElementDisplayName(elementKey);
|
||||
const settings = [];
|
||||
|
||||
@@ -600,18 +767,37 @@ function displayCurrentOverrides() {
|
||||
settings.push(`Size: ${override.size_px}px`);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="flex items-center justify-between p-3 bg-white rounded border">
|
||||
<div>
|
||||
<div class="font-medium text-gray-900">${elementName}</div>
|
||||
<div class="text-sm text-gray-600">${settings.join(', ')}</div>
|
||||
</div>
|
||||
<button onclick="deleteFontOverride('${elementKey}')" class="btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 text-sm">
|
||||
<i class="fas fa-trash mr-1"></i>Remove
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center justify-between p-3 bg-white rounded border';
|
||||
|
||||
const infoDiv = document.createElement('div');
|
||||
|
||||
const nameDiv = document.createElement('div');
|
||||
nameDiv.className = 'font-medium text-gray-900';
|
||||
nameDiv.textContent = elementName;
|
||||
|
||||
const settingsDiv = document.createElement('div');
|
||||
settingsDiv.className = 'text-sm text-gray-600';
|
||||
settingsDiv.textContent = settings.join(', ');
|
||||
|
||||
infoDiv.appendChild(nameDiv);
|
||||
infoDiv.appendChild(settingsDiv);
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'btn bg-red-600 hover:bg-red-700 text-white px-3 py-1 text-sm';
|
||||
const trashIcon = document.createElement('i');
|
||||
trashIcon.className = 'fas fa-trash mr-1';
|
||||
deleteBtn.appendChild(trashIcon);
|
||||
deleteBtn.appendChild(document.createTextNode('Remove'));
|
||||
deleteBtn.dataset.elementKey = elementKey;
|
||||
deleteBtn.addEventListener('click', function() {
|
||||
deleteFontOverride(this.dataset.elementKey);
|
||||
});
|
||||
|
||||
row.appendChild(infoDiv);
|
||||
row.appendChild(deleteBtn);
|
||||
container.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function getElementDisplayName(elementKey) {
|
||||
@@ -639,30 +825,75 @@ function getFontDisplayName(fontKey) {
|
||||
return names[fontKey] || fontKey;
|
||||
}
|
||||
|
||||
function updateFontPreview() {
|
||||
const canvas = document.getElementById('font-preview-canvas');
|
||||
const text = document.getElementById('preview-text').value || 'Sample Text';
|
||||
const family = document.getElementById('preview-family').value;
|
||||
const size = document.getElementById('preview-size').value;
|
||||
async function updateFontPreview() {
|
||||
const previewImage = document.getElementById('font-preview-image');
|
||||
const loadingText = document.getElementById('font-preview-loading');
|
||||
const textInput = document.getElementById('preview-text');
|
||||
const familySelect = document.getElementById('preview-family');
|
||||
const sizeSelect = document.getElementById('preview-size');
|
||||
|
||||
if (!canvas) return;
|
||||
if (!previewImage || !loadingText) return;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const text = textInput?.value || 'Sample Text 123';
|
||||
const family = familySelect?.value || '';
|
||||
const sizeToken = sizeSelect?.value || 'md';
|
||||
const sizePx = fontTokens[sizeToken] || 10;
|
||||
|
||||
// Set background
|
||||
ctx.fillStyle = '#000000';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
if (!family) {
|
||||
previewImage.style.display = 'none';
|
||||
loadingText.style.display = 'block';
|
||||
loadingText.textContent = 'Select a font to preview';
|
||||
return;
|
||||
}
|
||||
|
||||
// Set font properties
|
||||
const fontSize = fontTokens[size] || 8;
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.font = `${fontSize}px monospace`;
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
// Show loading state
|
||||
loadingText.textContent = 'Loading preview...';
|
||||
loadingText.style.display = 'block';
|
||||
previewImage.style.display = 'none';
|
||||
|
||||
// Draw text in center
|
||||
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
font: family,
|
||||
text: text,
|
||||
size: sizePx,
|
||||
bg: '000000',
|
||||
fg: 'ffffff'
|
||||
});
|
||||
|
||||
const response = await fetch(`${baseUrl}/api/v3/fonts/preview?${params}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message;
|
||||
try {
|
||||
const errorData = JSON.parse(text);
|
||||
message = errorData.message || `Server error: ${response.status}`;
|
||||
} catch {
|
||||
message = `Server error: ${response.status}`;
|
||||
}
|
||||
loadingText.textContent = message;
|
||||
loadingText.style.display = 'block';
|
||||
previewImage.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success' && data.data?.image) {
|
||||
previewImage.src = data.data.image;
|
||||
previewImage.style.display = 'block';
|
||||
loadingText.style.display = 'none';
|
||||
} else {
|
||||
loadingText.textContent = data.message || 'Failed to load preview';
|
||||
loadingText.style.display = 'block';
|
||||
previewImage.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading font preview:', error);
|
||||
loadingText.textContent = 'Error loading preview';
|
||||
loadingText.style.display = 'block';
|
||||
previewImage.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function initializeFontUpload() {
|
||||
@@ -671,13 +902,14 @@ function initializeFontUpload() {
|
||||
|
||||
function handleFileSelection(event) {
|
||||
const files = Array.from(event.target.files);
|
||||
const validExtensions = ['ttf', 'otf', 'bdf'];
|
||||
const validFiles = files.filter(file => {
|
||||
const extension = file.name.toLowerCase().split('.').pop();
|
||||
return extension === 'ttf' || extension === 'bdf';
|
||||
return validExtensions.includes(extension);
|
||||
});
|
||||
|
||||
if (validFiles.length === 0) {
|
||||
showNotification('Please select valid .ttf or .bdf font files', 'warning');
|
||||
showNotification('Please select valid .ttf, .otf, or .bdf font files', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -696,18 +928,26 @@ function showUploadForm() {
|
||||
const selectedFilesContainer = document.getElementById('selected-files');
|
||||
const fontFamilyInput = document.getElementById('upload-font-family');
|
||||
|
||||
// Show selected files
|
||||
selectedFilesContainer.innerHTML = selectedFontFiles.map(file => `
|
||||
<div class="flex items-center justify-between p-2 bg-gray-100 rounded">
|
||||
<span class="text-sm">${file.name} (${(file.size / 1024).toFixed(1)} KB)</span>
|
||||
</div>
|
||||
`).join('');
|
||||
// Show selected files using DOM APIs to prevent XSS
|
||||
selectedFilesContainer.innerHTML = '';
|
||||
selectedFontFiles.forEach(file => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'flex items-center justify-between p-2 bg-gray-100 rounded';
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.className = 'text-sm';
|
||||
span.textContent = `${file.name} (${(file.size / 1024).toFixed(1)} KB)`;
|
||||
|
||||
row.appendChild(span);
|
||||
selectedFilesContainer.appendChild(row);
|
||||
});
|
||||
|
||||
// Auto-generate font family name from first file
|
||||
if (selectedFontFiles.length === 1) {
|
||||
const filename = selectedFontFiles[0].name;
|
||||
const nameWithoutExt = filename.substring(0, filename.lastIndexOf('.'));
|
||||
fontFamilyInput.value = nameWithoutExt.toLowerCase().replace(/[^a-z0-9]/g, '_');
|
||||
// Preserve hyphens, convert other special chars to underscores
|
||||
fontFamilyInput.value = nameWithoutExt.toLowerCase().replace(/[^a-z0-9-]/g, '_');
|
||||
}
|
||||
|
||||
uploadForm.style.display = 'block';
|
||||
@@ -734,9 +974,9 @@ async function uploadSelectedFonts() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate font family name
|
||||
if (!/^[a-z0-9_]+$/i.test(fontFamily)) {
|
||||
showNotification('Font family name can only contain letters, numbers, and underscores', 'warning');
|
||||
// Validate font family name (must match backend validation)
|
||||
if (!/^[a-z0-9_-]+$/i.test(fontFamily)) {
|
||||
showNotification('Font family name can only contain letters, numbers, underscores, and hyphens', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -755,6 +995,19 @@ async function uploadSelectedFonts() {
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
let message;
|
||||
try {
|
||||
const errorData = JSON.parse(text);
|
||||
message = errorData.message || `Server error: ${response.status}`;
|
||||
} catch {
|
||||
message = `Server error: ${response.status}`;
|
||||
}
|
||||
showNotification(`Error uploading "${file.name}": ${message}`, 'error');
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.status === 'success') {
|
||||
@@ -773,6 +1026,10 @@ async function uploadSelectedFonts() {
|
||||
populateFontSelects();
|
||||
cancelFontUpload();
|
||||
hideUploadProgress();
|
||||
// Clear font-selector widget cache so new fonts appear in plugin configs
|
||||
if (typeof window.clearFontSelectorCache === 'function') {
|
||||
window.clearFontSelectorCache();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error uploading fonts:', error);
|
||||
@@ -850,7 +1107,12 @@ function updateUploadProgress(percent) {
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
#font-preview-canvas {
|
||||
#font-preview-container {
|
||||
max-width: 100%;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
#font-preview-image {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@@ -537,7 +537,7 @@
|
||||
{% else %}
|
||||
{% set str_widget = prop.get('x-widget') or prop.get('x_widget') %}
|
||||
{% set str_value = value if value is not none else (prop.default if prop.default is defined else '') %}
|
||||
{% if str_widget in ['text-input', 'textarea', 'select-dropdown', 'toggle-switch', 'radio-group', 'date-picker', 'slider', 'color-picker', 'email-input', 'url-input', 'password-input'] %}
|
||||
{% if str_widget in ['text-input', 'textarea', 'select-dropdown', 'toggle-switch', 'radio-group', 'date-picker', 'slider', 'color-picker', 'email-input', 'url-input', 'password-input', 'font-selector'] %}
|
||||
{# Render widget container #}
|
||||
<div id="{{ field_id }}_container" class="{{ str_widget }}-container"></div>
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user