diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6b3cf9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +siji.pcf +siji.otb +bin/__pycache__ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..423fec7 --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +DESTDIR = ~ +PREFIX = /.local +NAME = siji +X11DIR = $(DESTDIR)$(PREFIX)/share/fonts/$(NAME) +OTBDIR = $(DESTDIR)$(PREFIX)/share/fonts/$(NAME) + +build: pcf otb + +install: build + install -Dm644 siji.pcf $(X11DIR)/siji.pcf + install -Dm644 siji.otb $(OTBDIR)/siji.otb + +pcf: siji.bdf + bdftopcf siji.bdf -o siji.pcf + +otb: siji.bdf + python3 bin/otb1cli.py -o siji.otb siji.bdf + +clean: + rm siji.otb siji.pcf diff --git a/README.md b/README.md new file mode 100644 index 0000000..b808ae6 --- /dev/null +++ b/README.md @@ -0,0 +1,78 @@ +# Siji-ng + +A fixed and maintained [Siji](https://github.com/stark/siji) iconic bitmap font. + +**Contributions are welcome** + +![Siji-ng](preview.png "Preview of Siji") + +## Differences from Siji: +- Build **correct** OTB font => support in modern apps. +- New icons: `dwm_centeredmaster`, `dwm_centeredfloatingmaster`, `gentoo`, + `docker`. +- New repository structure, new build system, releases => easier to package. +- *More hopefully coming soon.* + +## Installation + +[![Packaging status]( +https://repology.org/badge/vertical-allrepos/siji-ng.svg)]( +https://repology.org/project/siji-ng/versions) + +#### Requirements: +- python3 +- bdftopcf + +```sh +git clone https://github.com/begss/siji-ng && cd siji-ng && make install +``` + +## How to get all glyphs + +```bash +#!/bin/bash + +codes="0 1 2 3 4 5 6 7 8 9 a b c d e f" + +for code0 in $codes; do + for code1 in $codes; do + for code2 in $codes; do + printf "e$code0$code1$code2 - \ue$code0$code1$code2\n" + done + done +done +``` + +## TODO + +- [ ] More Glyphs! +- [ ] Adding glyphs of different sizes. +- [ ] Improving glyph alignment. +- [ ] Creating small and large version of Siji. + +## Credits + +[**stark**](https://github.com/stark) for [Siji](https://github.com/stark/siji). + +[**wrkzk**](https://github.com/wrkzk) for `dwm_centeredmaster`, +`dwm_centeredfloatingmaster` and `gentoo` icons. + +[**the-papi**](https://github.com/the-papi) for `docker` icon. + +**Dimitar Zhekov** for bdf to otb convertation scripts. + +**Sm4tik** for sm4tik xbm icon pack. + +**Stlarch** for stlarch font. + +**Sunaku** for sm4tik font. + +**Lokaltog** for symbols font. + +**w0ng** for xbm icon font. + +**Dave Gandy** for FontAwesome. + +**Lucy** for Tewi font. + +**Phallus** for Lemon and Uushi font. diff --git a/Readme.md b/Readme.md deleted file mode 100644 index 91f3c22..0000000 --- a/Readme.md +++ /dev/null @@ -1,130 +0,0 @@ -# Siji - -Siji is an iconic bitmap font based on the Stlarch font with additional glyphs. - -It inherits additional glyphs from Sm4tik xbm icon pack, Lokaltog Symbols font, xbm-icon font, Uushi font, FontAwesome, and Tewi font with personal additions. - -Siji is built on top of Stlarch, so your existing stlarch unicode values will **just work**. - -Siji is a **Work In Progress**, so more glyphs will be added over the time. - -![Siji](preview.png "Preview of Siji") - -## What's the difference between Stlarch and Siji ? - -- Siji comes with **215 new glyphs**, selectively ported from other fonts and xbm icons with personal additions. -- Increased letter spacing. -- Properly centered glyphs. - -## Installation - -### Arch Linux - -[PKGBUILD](https://aur.archlinux.org/packages/siji-git) by [brandon99](https://github.com/brandon099) - -### CRUX - -[CRUX port](http://dl.z3bra.org/crux/v3.2/siji-font/) by [z3bra](http://z3bra.org/) - -### Others - -Clone this repo and `cd` into the siji directory: - -```sh -git clone https://github.com/stark/siji && cd siji -``` - -Then run the installer script with `./install.sh`. - -By default Siji will be installed in your `$HOME/.fonts` directory, it will be created if the directory is non-existent. - -If you wish to install Siji in another directory then run the `install.sh` script with the `-d` flag and specify the font directory as an argument. - -**Example:** - -```sh -./install.sh -d ~/.fonts -``` - -## How to get the glyph codes ? - -Install `xfd`: - -### Arch Linux -``` -sudo pacman -S xorg-xfd -``` - -### Debian -``` -sudo apt-get install x11-utils -``` - -### Fedora -``` -sudo dnf install xorg-x11-apps -``` - -After installing `xfd` run the `view.sh` script: - -```sh -./view.sh -``` - -## Using Siji with other programs: - -As long as the program allows to set a fallback font, Siji will work. - -Examples of using Siji with some programs: - -### Dwm - -```C -static const char font[] = "-wuncon-siji-medium-r-normal--10-100-75-75-c-80-iso10646-1" "," /* For Iconic Glyphs */ - "-*-tamsyn-medium-r-normal-*-12-*-*-*-*-*-*-1"; /* For Normal Text */ -``` - -### Dmenu - -```sh -dmenu -fn '-*-tamsyn-medium-r-normal-*-12-*-*-*-*-*-*-1','-wuncon-siji-medium-r-normal--10-100-75-75-c-80-iso10646-1' -``` - -### Lemonbar - -```sh -lemonbar -p -f '-*-tamsyn-medium-r-normal-*-12-*-*-*-*-*-*-1' -f '-wuncon-siji-medium-r-normal--10-100-75-75-c-80-iso10646-1' -``` - -## TODO - -- [ ] More Glyphs! -- [ ] Adding glyphs of different sizes. -- [ ] Improving glyph alignment. -- [ ] Creating small and large version of siji. - -## Credits - -A Big Thanks to the following Authors for their spectacular work which made Siji possible: - -**Sm4tik** for sm4tik xbm icon pack - -**Stlarch** for stlarch font - -**Sunaku** for sm4tik font - -**Lokaltog** for symbols font - -**w0ng** for xbm icon font - -**Dave Gandy** for FontAwesome - -**Lucy** for Tewi font - -**Phallus** for Lemon and Uushi font - -## License - -Siji is licensed under GPLv2. - -See `LICENSE` file for copyright details. diff --git a/bin/LICENSE b/bin/LICENSE new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/bin/LICENSE @@ -0,0 +1,339 @@ + 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. + + + Copyright (C) + + 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. + + , 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. diff --git a/bin/bdf.py b/bin/bdf.py new file mode 100644 index 0000000..8ce8cb3 --- /dev/null +++ b/bin/bdf.py @@ -0,0 +1,309 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import re +import codecs +from collections import OrderedDict +from enum import IntEnum, unique + +import fnutil + +# -- Width -- +DPARSE_LIMIT = 512 +SPARSE_LIMIT = 32000 + +class Width: + def __init__(self, x, y): + self.x = x + self.y = y + + + @staticmethod + def parse(name, value, limit): + words = fnutil.split_words(name, value, 2) + return Width(fnutil.parse_dec(name + '.x', words[0], -limit, limit), + fnutil.parse_dec(name + '.y', words[1], -limit, limit)) + + + @staticmethod + def parse_s(name, value): + return Width.parse(name, value, SPARSE_LIMIT) + + + @staticmethod + def parse_d(name, value): + return Width.parse(name, value, DPARSE_LIMIT) + + + def __str__(self): + return '%d %d' % (self.x, self.y) + + +# -- BXX -- +class BBX: + def __init__(self, width, height, xoff, yoff): + self.width = width + self.height = height + self.xoff = xoff + self.yoff = yoff + + + @staticmethod + def parse(name, value): + words = fnutil.split_words(name, value, 4) + return BBX(fnutil.parse_dec('width', words[0], 1, DPARSE_LIMIT), + fnutil.parse_dec('height', words[1], 1, DPARSE_LIMIT), + fnutil.parse_dec('bbxoff', words[2], -DPARSE_LIMIT, DPARSE_LIMIT), + fnutil.parse_dec('bbyoff', words[3], -DPARSE_LIMIT, DPARSE_LIMIT)) + + + def row_size(self): + return (self.width + 7) >> 3 + + + def __str__(self): + return '%d %d %d %d' % (self.width, self.height, self.xoff, self.yoff) + + +# -- Props -- +def skip_comments(line): + return None if line[:7] == b'COMMENT' else line + + +class Props(OrderedDict): + def __iter__(self): + return self.items().__iter__() + + + def read(self, input, name, callback=None): + return self.parse(input.read_lines(skip_comments), name, callback) + + + def parse(self, line, name, callback=None): + if not line or not line.startswith(bytes(name, 'ascii')): + raise Exception(name + ' expected') + + value = line[len(name):].lstrip() + self[name] = value + return value if callback is None else callback(name, value) + + + def set(self, name, value): + self[name] = value if isinstance(value, (bytes, bytearray)) else bytes(str(value), 'ascii') + + +# -- Base -- +class Base: + def __init__(self): + self.props = Props() + self.bbx = None + + +# -- Char +HEX_BYTES = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70) + +class Char(Base): + def __init__(self): + Base.__init__(self) + self.code = -1 + self.swidth = None + self.dwidth = None + self.data = None + + + def bitmap(self): + bitmap = '' + row_size = self.bbx.row_size() + + for index in range(0, len(self.data), row_size): + bitmap += self.data[index : index + row_size].hex() + '\n' + + return bytes(bitmap, 'ascii').upper() + + + def _read(self, input): + # HEADER + self.props.read(input, 'STARTCHAR') + self.code = self.props.read(input, 'ENCODING', fnutil.parse_dec) + self.swidth = self.props.read(input, 'SWIDTH', Width.parse_s) + self.dwidth = self.props.read(input, 'DWIDTH', Width.parse_d) + self.bbx = self.props.read(input, 'BBX', BBX.parse) + line = input.read_lines(skip_comments) + + if line and line.startswith(b'ATTRIBUTES'): + self.props.parse(line, 'ATTRIBUTES') + line = input.read_lines(skip_comments) + + # BITMAP + if self.props.parse(line, 'BITMAP'): + raise Exception('BITMAP expected') + + row_len = self.bbx.row_size() * 2 + self.data = bytearray() + + for _ in range(0, self.bbx.height): + line = input.read_lines(skip_comments) + + if not line: + raise Exception('bitmap data expected') + + if len(line) == row_len: + self.data += codecs.decode(line, 'hex') + else: + raise Exception('invalid bitmap length') + + # FINAL + if input.read_lines(skip_comments) != b'ENDCHAR': + raise Exception('ENDCHAR expected') + + return self + + + @staticmethod + def read(input): + return Char()._read(input) # pylint: disable=protected-access + + + def write(self, output): + for [name, value] in self.props: + output.write_prop(name, value) + + output.write_line(self.bitmap() + b'ENDCHAR') + + +# -- Font -- +@unique +class XLFD(IntEnum): + FOUNDRY = 1 + FAMILY_NAME = 2 + WEIGHT_NAME = 3 + SLANT = 4 + SETWIDTH_NAME = 5 + ADD_STYLE_NAME = 6 + PIXEL_SIZE = 7 + POINT_SIZE = 8 + RESOLUTION_X = 9 + RESOLUTION_Y = 10 + SPACING = 11 + AVERAGE_WIDTH = 12 + CHARSET_REGISTRY = 13 + CHARSET_ENCODING = 14 + +CHARS_MAX = 65535 + +class Font(Base): + def __init__(self): + Base.__init__(self) + self.xlfd = [] + self.chars = [] + self.default_code = -1 + + + @property + def bold(self): + return b'bold' in self.xlfd[XLFD.WEIGHT_NAME].lower() + + + @property + def italic(self): + return self.xlfd[XLFD.SLANT] in [b'I', b'O'] + + + @property + def proportional(self): + return self.xlfd[XLFD.SPACING] == b'P' + + + def _read(self, input): + # HEADER + line = input.read_line() + + if self.props.parse(line, 'STARTFONT') != b'2.1': + raise Exception('STARTFONT 2.1 expected') + + self.xlfd = self.props.read(input, 'FONT', lambda name, value: value.split(b'-', 15)) + + if len(self.xlfd) != 15 or self.xlfd[0] != b'': + raise Exception('non-XLFD font names are not supported') + + self.props.read(input, 'SIZE') + self.bbx = self.props.read(input, 'FONTBOUNDINGBOX', BBX.parse) + line = input.read_lines(skip_comments) + + if line and line.startswith(b'STARTPROPERTIES'): + num_props = self.props.parse(line, 'STARTPROPERTIES', fnutil.parse_dec) + + for _ in range(0, num_props): + line = input.read_lines(skip_comments) + + if line is None: + raise Exception('property expected') + + match = re.fullmatch(br'(\w+)\s+([-\d"].*)', line) + + if not match: + raise Exception('invalid property format') + + name = str(match.group(1), 'ascii') + value = match.group(2) + + if self.props.get(name) is not None: + raise Exception('duplicate property') + + if name == 'DEFAULT_CHAR': + self.default_code = fnutil.parse_dec(name, value) + + self.props[name] = value + + if self.props.read(input, 'ENDPROPERTIES') != b'': + raise Exception('ENDPROPERTIES expected') + + line = input.read_lines(skip_comments) + + # GLYPHS + num_chars = fnutil.parse_dec('CHARS', self.props.parse(line, 'CHARS'), 1, CHARS_MAX) + + for _ in range(0, num_chars): + self.chars.append(Char.read(input)) + + #if next((char.code for char in self.chars if char.code == self.default_code), -1) != self.default_code: + #raise Exception('invalid DEFAULT_CHAR') + + # FINAL + if input.read_lines(skip_comments) != b'ENDFONT': + raise Exception('ENDFONT expected') + + if input.read_line() is not None: + raise Exception('garbage after ENDFONT') + + return self + + + @staticmethod + def read(input): + return Font()._read(input) # pylint: disable=protected-access + + + def write(self, output): + for [name, value] in self.props: + output.write_prop(name, value) + + for char in self.chars: + char.write(output) + + output.write_line(b'ENDFONT') diff --git a/bin/bdfcheck.py b/bin/bdfcheck.py new file mode 100644 index 0000000..c7790f0 --- /dev/null +++ b/bin/bdfcheck.py @@ -0,0 +1,380 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import re +import codecs +from collections import OrderedDict +from enum import IntEnum, unique + +import fnutil +import fncli +import fnio +import bdf + +# -- Params -- +class Params(fncli.Params): # pylint: disable=too-many-instance-attributes + def __init__(self): + fncli.Params.__init__(self) + self.ascii_chars = True + self.bbx_exceeds = True + self.dupl_codes = -1 + self.extra_bits = True + self.attributes = True + self.dupl_names = -1 + self.dupl_props = True + self.common_slant = True + self.common_weight = True + self.xlfd_fontnm = True + self.ywidth_zero = True + + +# -- Options -- +HELP = ('' + + 'usage: bdfcheck [options] [INPUT...]\n' + + 'Check BDF font(s) for various problems\n' + + '\n' + + ' -A disable non-ascii characters check\n' + + ' -B disable BBX exceeding FONTBOUNDINGBOX checks\n' + + ' -c/-C enable/disable duplicate character codes check\n' + + ' (default = enabled for registry ISO10646)\n' + + ' -E disable extra bits check\n' + + ' -I disable ATTRIBUTES check\n' + + ' -n/-N enable duplicate character names check\n' + + ' (default = enabled for registry ISO10646)\n' + + ' -P disable duplicate properties check\n' + + ' -S disable common slant check\n' + + ' -W disable common weight check\n' + + ' -X disable XLFD font name check\n' + + ' -Y disable zero WIDTH Y check\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'File directives: COMMENT bdfcheck --enable|disable-\n' + + ' (also available as long command line options)\n' + + '\n' + + 'Check names: ascii-chars, bbx-exceeds, duplicate-codes, extra-bits,\n' + + ' attributes, duplicate-names, duplicate-properties, common-slant,\n' + + ' common-weight, xlfd-font, ywidth-zero\n' + + '\n' + + 'The input BDF(s) must be v2.1 with unicode encoding.\n') + +VERSION = 'bdfcheck 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, [], HELP, VERSION) + + + def parse(self, name, directive, params): + value = name.startswith('--enable') or name[1].islower() + + if name in ['-A', '--enable-ascii-chars', '--disable-ascii-chars']: + params.ascii_chars = value + elif name in ['-B', '--enable-bbx-exceeds', '--disable-bbx-exceeds']: + params.bbx_exceeds = value + elif name in ['-c', '-C', '--enable-duplicate-codes', '--disable-duplicate-codes']: + params.dupl_codes = value + elif name in ['-E', '--enable-extra-bits', '--disable-extra-bits']: + params.extra_bits = value + elif name in ['-I', '--enable-attributes', '--disable-attributes']: + params.attributes = value + elif name in ['-n', '-N', '--enable-duplicate-names', '--disable-duplicate-names']: + params.dupl_names = value + elif name in ['-P', '--enable-duplicate-properties', '--disable-duplicate-properties']: + params.dupl_props = value + elif name in ['-S', '--enable-common-slant', '--disable-common-slant']: + params.common_slant = value + elif name in ['-W', '--enable-common-weight', '--disable-common-weight']: + params.common_weight = value + elif name in ['-X', '--enable-xlfd-font', '--disable-xlfd-font']: + params.xlfd_fontnm = value + elif name in ['-Y', '--enable-ywidth-zero', '--disable-ywidth-zero']: + params.ywidth_zero = value + else: + return directive is not True and self.fallback(name, params) + + return directive is not True or name.startswith('--') + + +# -- DupMap -- +class DupMap(OrderedDict): + def __init__(self, prefix, severity, descript, quote): + OrderedDict.__init__(self) + self.prefix = prefix + self.descript = descript + self.severity = severity + self.quote = quote + + + def check(self): + for value, lines in self.items(): + if len(lines) > 1: + text = 'duplicate %s %s at lines' % (self.descript, str(value)) + + for index, line in enumerate(lines): + text += ' ' if index == 0 else ' and ' if index == len(lines) - 1 else ', ' + text += str(line) + + fnutil.message(self.prefix, self.severity, text) + + + def push(self, value, line_no): + try: + self[value].append(line_no) + except KeyError: + self[value] = [line_no] + + +# -- InputFileStream -- +@unique +class MODE(IntEnum): + META = 0 + PROPS = 1 + BITMAP = 2 + +class InputFileStream(fnio.InputFileStream): + def __init__(self, file_name, parsed): + fnio.InputFileStream.__init__(self, file_name) + self.parsed = parsed + self.mode = MODE.META + self.proplocs = DupMap(self.location(), 'error', 'property', '') + self.namelocs = DupMap(self.location(), 'warning', 'character name', '"') + self.codelocs = DupMap(self.location(), 'warning', 'encoding', '') + self.handlers = [ + (b'STARTCHAR', lambda value: self.append_name(value)), + (b'ENCODING', lambda value: self.append_code(value)), + (b'SWIDTH', lambda value: self.check_width('SWIDTH', value, bdf.Width.parse_s)), + (b'DWIDTH', lambda value: self.check_width('DWIDTH', value, bdf.Width.parse_d)), + (b'BBX', lambda value: self.set_last_box(value)), + (b'BITMAP', lambda _: self.set_mode(MODE.BITMAP)), + (b'SIZE', InputFileStream.check_size), + (b'ATTRIBUTES', lambda value: self.check_attr(value)), + (b'STARTPROPERTIES', lambda _: self.set_mode(MODE.PROPS)), + (b'FONTBOUNDINGBOX', lambda value: self.set_font_box(value)), + ] + self.xlfd_name = False + self.last_box = None + self.font_box = None + self.options = Options() + + + def append(self, option, valocs, value): + if option: + valocs.push(str(value, 'ascii'), self.line_no) + + + def append_code(self, value): + fnutil.parse_dec('encoding', value) + self.append(self.parsed.dupl_codes, self.codelocs, value) + + + def append_name(self, value): + self.append(self.parsed.dupl_names, self.namelocs, b'"%s"' % value) + + + def check_width(self, name, value, parse): + if self.parsed.ywidth_zero and parse(name, value).y != 0: + fnutil.warning(self.location(), 'non-zero %s Y' % name) + + + def set_font_box(self, value): + self.font_box = bdf.BBX.parse('FONTBOUNDINGBOX', value) + + + def set_last_box(self, value): + bbx = bdf.BBX.parse('BBX', value) + + if self.parsed.bbx_exceeds: + exceeds = [] + + if bbx.xoff < self.font_box.xoff: + exceeds.append('xoff < FONTBOUNDINGBOX xoff') + + if bbx.yoff < self.font_box.yoff: + exceeds.append('yoff < FONTBOUNDINGBOX yoff') + + if bbx.width > self.font_box.width: + exceeds.append('width > FONTBOUNDINGBOX width') + + if bbx.height > self.font_box.height: + exceeds.append('height > FONTBOUNDINGBOX height') + + for exceed in exceeds: + fnutil.message(self.location(), '', exceed) + + self.last_box = bbx + + + def set_mode(self, new_mode): + self.mode = new_mode + + + def check(self): + self.process(bdf.Font.read) + self.proplocs.check() + self.namelocs.check() + self.codelocs.check() + + + @staticmethod + def check_size(value): + words = fnutil.split_words('SIZE', value, 3) + fnutil.parse_dec('point size', words[0], 1, None) + fnutil.parse_dec('x resolution', words[1], 1, None) + fnutil.parse_dec('y resolution', words[2], 1, None) + + + def check_attr(self, value): + if not re.fullmatch(br'[\dA-Fa-f]{4}', value): + raise Exception('ATTRIBUTES must be 4 hex-encoded characters') + + if self.parsed.attributes: + fnutil.warning(self.location(), 'ATTRIBUTES may cause problems with freetype') + + + def check_font(self, value): + xlfd = value[4:].lstrip().split(b'-', 15) + + if len(xlfd) == 15 and xlfd[0] == b'': + unicode = (xlfd[bdf.XLFD.CHARSET_REGISTRY].upper() == b'ISO10646') + + if self.parsed.dupl_codes == -1: + self.parsed.dupl_codes = unicode + + if self.parsed.dupl_names == -1: + self.parsed.dupl_names = unicode + + if self.parsed.common_weight: + weight = str(xlfd[bdf.XLFD.WEIGHT_NAME], 'ascii') + compare = weight.lower() + consider = 'Bold' if 'bold' in compare else 'Normal' + + if compare in ['medium', 'regular']: + compare = 'normal' + + if compare != consider.lower(): + fnutil.warning(self.location(), 'weight "%s" may be considered %s' % (weight, consider)) + + if self.parsed.common_slant: + slant = str(xlfd[bdf.XLFD.SLANT], 'ascii') + consider = 'Italic' if re.search('^[IO]', slant) else 'Regular' + + if not re.fullmatch('[IOR]', slant): + fnutil.warning(self.location(), 'slant "%s" may be considered %s' % (slant, consider)) + + else: + if self.parsed.xlfd_fontnm: + fnutil.warning(self.location(), 'non-XLFD font name') + + value = b'FONT --------------' + + return value + + + def check_prop(self, line): + match = re.fullmatch(br'(\w+)\s+([-\d"].*)', line) + + if not match: + raise Exception('invalid property format') + + name = match.group(1) + value = match.group(2) + + if value.startswith(b'"'): + if len(value) < 2 or not value.endswith(b'"'): + raise Exception('no closing double quote') + if re.search(b'[^"]"[^"]', value[1 : len(value) - 1]): + raise Exception('unescaped double quote') + else: + fnutil.parse_dec('value', value, None, None) + + self.append(self.parsed.dupl_props, self.proplocs, name) + return b'P%d 1' % self.line_no + + + def check_bitmap(self, line): + if len(line) != self.last_box.row_size() * 2: + raise Exception('invalid bitmap length') + + data = codecs.decode(line, 'hex') + + if self.parsed.extra_bits: + check_x = (self.last_box.width - 1) | 7 + last_byte = data[len(data) - 1] + bit_no = 7 - (self.last_box.width & 7) + + for x in range(self.last_box.width, check_x + 1): + if last_byte & (1 << bit_no): + fnutil.warning(self.location(), 'extra bit(s) starting with x=%d' % x) + break + bit_no -= 1 + + + def check_line(self, line): + if re.search(b'[^\t\f\v\x20-\xff]', line): + raise Exception('control character(s)') + + if self.parsed.ascii_chars and re.search(b'[\x7f-\xff]', line): + fnutil.warning(self.location(), 'non-ascii character(s)') + + if self.mode == MODE.META: + if not self.xlfd_name and line.startswith(b'FONT'): + line = self.check_font(line) + self.xlfd_name = True + else: + for handler in self.handlers: + if line.startswith(handler[0]): + handler[1](line[len(handler[0]):].lstrip()) + break + elif self.mode == MODE.PROPS: + if line.startswith(b'ENDPROPERTIES'): + self.mode = MODE.META + else: + line = self.check_prop(line) + else: # MODE.BITMAP + if line.startswith(b'ENDCHAR'): + self.mode = MODE.META + else: + self.check_bitmap(line) + + return line + + + def read_check(self, line, callback): + match = re.search(br'^COMMENT\s*bdfcheck\s+(-.*)$', line) + + if match and not self.options.parse(str(match[1], 'ascii'), True, self.parsed): + raise Exception('invalid bdfcheck directive') + + line = callback(line) + return self.check_line(line) if line is not None else None + + + def read_lines(self, callback): + return fnio.InputFileStream.read_lines(self, lambda line: self.read_check(line, callback)) + + +# -- Main -- +def main_program(nonopt, parsed): + for input_name in nonopt or [None]: + InputFileStream(input_name, parsed).check() + + +if __name__ == '__main__': + fncli.start('bdfcheck.py', Options(), Params(), main_program) diff --git a/bin/bdfexp.py b/bin/bdfexp.py new file mode 100644 index 0000000..8409935 --- /dev/null +++ b/bin/bdfexp.py @@ -0,0 +1,245 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +from collections import OrderedDict + +import fnutil +import fncli +import fnio +import bdf + +# -- Font -- +class Font(bdf.Font): + def __init__(self): + bdf.Font.__init__(self) + self.min_width = 0 # used in proportional() + self.avg_width = 0 + + + def _expand(self, char): + if char.dwidth.x >= 0: + if char.bbx.xoff >= 0: + width = max(char.bbx.xoff + char.bbx.width, char.dwidth.x) + dst_xoff = char.bbx.xoff + exp_xoff = 0 + else: + width = max(char.bbx.width, char.dwidth.x - char.bbx.xoff) + dst_xoff = 0 + exp_xoff = char.bbx.xoff + else: + rev_xoff = char.bbx.xoff + char.bbx.width + + if rev_xoff <= 0: + width = -min(char.dwidth.x, char.bbx.xoff) + dst_xoff = width + char.bbx.xoff + exp_xoff = -width + else: + width = max(char.bbx.width, rev_xoff - char.dwidth.x) + dst_xoff = width - char.bbx.width + exp_xoff = rev_xoff - width + + height = self.bbx.height + + if width == char.bbx.width and height == char.bbx.height: + return + + src_row_size = char.bbx.row_size() + dst_row_size = (width + 7) >> 3 + dst_ymax = self.px_ascender - char.bbx.yoff + dst_ymin = dst_ymax - char.bbx.height + copy_row = (dst_xoff & 7) == 0 + dst_data = bytearray(dst_row_size * height) + + for dst_y in range(dst_ymin, dst_ymax): + src_byte_no = (dst_y - dst_ymin) * src_row_size + dst_byte_no = dst_y * dst_row_size + (dst_xoff >> 3) + + if copy_row: + dst_data[dst_byte_no : dst_byte_no + src_row_size] = \ + char.data[src_byte_no : src_byte_no + src_row_size] + else: + src_bit_no = 7 + dst_bit_no = 7 - (dst_xoff & 7) + + for _ in range(0, char.bbx.width): + if char.data[src_byte_no] & (1 << src_bit_no): + dst_data[dst_byte_no] |= (1 << dst_bit_no) + + if src_bit_no > 0: + src_bit_no -= 1 + else: + src_bit_no = 7 + src_byte_no += 1 + + if dst_bit_no > 0: + dst_bit_no -= 1 + else: + dst_bit_no = 7 + dst_byte_no += 1 + + char.bbx = bdf.BBX(width, height, exp_xoff, self.bbx.yoff) + char.props.set('BBX', char.bbx) + char.data = dst_data + + + def expand(self): + # PREXPAND / VERTICAL + ascent = self.props.get('FONT_ASCENT') + descent = self.props.get('FONT_DESCENT') + px_ascent = 0 if ascent is None else fnutil.parse_dec('FONT_ASCENT', ascent, 0, bdf.DPARSE_LIMIT) + px_descent = 0 if descent is None else fnutil.parse_dec('FONT_DESCENT', descent, 0, bdf.DPARSE_LIMIT) + + for char in self.chars: + px_ascent = max(px_ascent, char.bbx.height + char.bbx.yoff) + px_descent = max(px_descent, -char.bbx.yoff) + + self.bbx.height = px_ascent + px_descent + self.bbx.yoff = -px_descent + + # EXPAND / HORIZONTAL + total_width = 0 + self.min_width = self.chars[0].bbx.width + + for char in self.chars: + self._expand(char) + self.min_width = min(self.min_width, char.bbx.width) + self.bbx.width = max(self.bbx.width, char.bbx.width) + self.bbx.xoff = min(self.bbx.xoff, char.bbx.xoff) + total_width += char.bbx.width + + self.avg_width = round(total_width / len(self.chars)) + self.props.set('FONTBOUNDINGBOX', self.bbx) + + + def expand_x(self): + for char in self.chars: + if char.dwidth.x != char.bbx.width: + char.swidth.x = round(char.bbx.width * 1000 / self.bbx.height) + char.props.set('SWIDTH', char.swidth) + char.dwidth.x = char.bbx.width + char.props.set('DWIDTH', char.dwidth) + + char.bbx.xoff = 0 + char.props.set('BBX', char.bbx) + + self.bbx.xoff = 0 + self.props.set('FONTBOUNDINGBOX', self.bbx) + + + def expand_y(self): + props = OrderedDict(( + ('FONT_ASCENT', self.px_ascender), + ('FONT_DESCENT', -self.px_descender), + ('PIXEL_SIZE', self.bbx.height) + )) + + for [name, value] in props.items(): + if self.props.get(name) is not None: + self.props.set(name, value) + + self.xlfd[bdf.XLFD.PIXEL_SIZE] = bytes(str(self.bbx.height), 'ascii') + self.props.set('FONT', b'-'.join(self.xlfd)) + + + @property + def proportional(self): + return self.bbx.width > self.min_width or bdf.Font.proportional.fget(self) # pylint: disable=no-member + + @property + def px_ascender(self): + return self.bbx.height + self.bbx.yoff + + @property + def px_descender(self): + return self.bbx.yoff + + + def _read(self, input): + bdf.Font._read(self, input) + self.expand() + return self + + @staticmethod + def read(input): + return Font()._read(input) # pylint: disable=protected-access + + +# -- Params -- +class Params(fncli.Params): + def __init__(self): + fncli.Params.__init__(self) + self.expand_x = False + self.expand_y = False + self.output_name = None + + +# -- Options -- +HELP = ('' + + 'usage: bdfexp [-X] [-Y] [-o OUTPUT] [INPUT]\n' + + 'Expand BDF font bitmaps\n' + + '\n' + + ' -X zero xoffs, set character S/DWIDTH.X from the output\n' + + ' BBX.width if needed\n' + + ' -Y enlarge FONT_ASCENT, FONT_DESCENT and PIXEL_SIZE to\n' + + ' cover the font bounding box, if needed\n' + + ' -o OUTPUT output file (default = stdout)\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n') + +VERSION = 'bdfexp 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, ['-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name == '-X': + params.expand_x = True + elif name == '-Y': + params.expand_y = True + elif name == '-o': + params.output_name = value + else: + self.fallback(name, params) + + +# -- Main -- +def main_program(nonopt, parsed): + if len(nonopt) > 1: + raise Exception('invalid number of arguments, try --help') + + # READ INPUT + font = fnio.read_file(nonopt[0] if nonopt else None, Font.read) + + # EXTRA ACTIONS + if parsed.expand_x: + font.expand_x() + + if parsed.expand_y: + font.expand_y() + + # WRITE OUTPUT + fnio.write_file(parsed.output_name, lambda ofs: font.write(ofs)) + + +if __name__ == '__main__': + fncli.start('bdfexp.py', Options(), Params(), main_program) diff --git a/bin/bdftofnt.py b/bin/bdftofnt.py new file mode 100644 index 0000000..0bfeed8 --- /dev/null +++ b/bin/bdftofnt.py @@ -0,0 +1,222 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import re + +import fnutil +import fncli +import fnio +import bdf +import bdfexp + +# -- Params -- +class Params(fncli.Params): + def __init__(self): + fncli.Params.__init__(self) + self.char_set = -1 + self.min_char = -1 + self.fnt_family = 0 + self.output_name = None + + +# -- Options -- +HELP = ('' + + 'usage: bdftofnt [-c CHARSET] [-m MINCHAR] [-f FAMILY] [-o OUTPUT] [INPUT]\n' + + 'Convert a BDF font to Windows FNT\n' + + '\n' + + ' -c CHARSET fnt character set (default = 0, see wingdi.h ..._CHARSET)\n' + + ' -m MINCHAR fnt minimum character code (8-bit CP decimal, not unicode)\n' + + ' -f FAMILY fnt family: DontCare, Roman, Swiss, Modern or Decorative\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n') + +VERSION = 'bdftofnt 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +FNT_FAMILIES = ['DontCare', 'Roman', 'Swiss', 'Modern', 'Decorative'] + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, ['-c', '-m', '-f', '-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name == '-c': + params.char_set = fnutil.parse_dec('CHARSET', value, 0, 255) + elif name == '-m': + params.min_char = fnutil.parse_dec('MINCHAR', value, 0, 255) + elif name == '-f': + if value in FNT_FAMILIES: + params.fnt_family = FNT_FAMILIES.index(value) + else: + raise Exception('invalid FAMILY') + elif name == '-o': + params.output_name = value + else: + self.fallback(name, params) + + +# -- Main -- +FNT_HEADER_SIZE = 118 +FNT_CHARSETS = [238, 204, 0, 161, 162, 177, 178, 186, 163] + +def main_program(nonopt, parsed): + if len(nonopt) > 1: + raise Exception('invalid number of arguments, try --help') + + char_set = parsed.char_set + min_char = parsed.min_char + + # READ INPUT + ifs = fnio.InputFileStream(nonopt[0] if nonopt else None) + font = ifs.process(bdfexp.Font.read) + + # COMPUTE + if char_set == -1: + encoding = font.xlfd[bdf.XLFD.CHARSET_ENCODING] + + if re.fullmatch(b'(cp)?125[0-8]', encoding.lower()): + char_set = FNT_CHARSETS[int(encoding[-1:])] + else: + char_set = 255 + + try: + num_chars = len(font.chars) + + if num_chars > 256: + raise Exception('too many characters, the maximum is 256') + + if min_char == -1: + if num_chars in [192, 256]: + min_char = 256 - num_chars + else: + min_char = font.chars[0].code + + max_char = min_char + num_chars - 1 + + if max_char >= 256: + raise Exception('the maximum character code is too big, (re)specify -m') + + # HEADER + vtell = FNT_HEADER_SIZE + (num_chars + 1) * 4 + bits_offset = vtell + ctable = [] + width_bytes = 0 + + # CTABLE/GLYPHS + for char in font.chars: + row_size = char.bbx.row_size() + ctable.append(char.bbx.width) + ctable.append(vtell) + vtell += row_size * font.bbx.height + width_bytes += row_size + + if vtell > 0xFFFF: + raise Exception('too much character data') + + # SENTINEL + sentinel = 2 - width_bytes % 2 + ctable.append(sentinel * 8) + ctable.append(vtell) + vtell += sentinel * font.bbx.height + width_bytes += sentinel + + if width_bytes > 0xFFFF: + raise Exception('the total character width is too big') + + except Exception as ex: + ex.message = ifs.location() + getattr(ex, 'message', str(ex)) + raise + + # WRITE + def write_fnt(output): + # HEADER + family = font.xlfd[bdf.XLFD.FAMILY_NAME] + copyright = font.props.get('COPYRIGHT') + copyright = fnutil.unquote(copyright)[:60] if copyright is not None else b'' + + output.write16(0x0200) # font version + output.write32(vtell + len(family) + 1) # total size + output.write_zstr(copyright, 60 - len(copyright)) + output.write16(0) # gdi, device type + output.write16(round(font.bbx.height * 72 / 96)) + output.write16(96) # vertical resolution + output.write16(96) # horizontal resolution + output.write16(font.px_ascender) # base line + output.write16(0) # internal leading + output.write16(0) # external leading + output.write8(int(font.italic)) + output.write8(0) # underline + output.write8(0) # strikeout + output.write16(700 if font.bold else 400) + output.write8(char_set) + output.write16(0 if font.proportional else font.bbx.width) + output.write16(font.bbx.height) + output.write8((parsed.fnt_family << 4) + int(font.proportional)) + output.write16(font.avg_width) + output.write16(font.bbx.width) + output.write8(min_char) + output.write8(max_char) + + default_index = max_char - min_char + break_index = 0 + + if font.default_code != -1: + default_index = next(index for index, char in enumerate(font.chars) if char.code == font.default_code) + + if min_char <= 0x20 <= max_char: + break_index = 0x20 - min_char + + output.write8(default_index) + output.write8(break_index) + output.write16(width_bytes) + output.write32(0) # device name + output.write32(vtell) + output.write32(0) # gdi bits pointer + output.write32(bits_offset) + output.write8(0) # reserved + + # CTABLE + for value in ctable: + output.write16(value) + + # GLYPHS + data = bytearray(font.bbx.height * font.bbx.row_size()) + + for char in font.chars: + row_size = char.bbx.row_size() + counter = 0 + # MS coordinates + for n in range(0, row_size): + for y in range(0, font.bbx.height): + data[counter] = char.data[row_size * y + n] + counter += 1 + output.write(data[:counter]) + output.write(bytes(sentinel * font.bbx.height)) + + # FAMILY + output.write_zstr(family, 1) + + fnio.write_file(parsed.output_name, write_fnt, encoding=None) + + +if __name__ == '__main__': + fncli.start('bdftofnt.py', Options(), Params(), main_program) diff --git a/bin/bdftopsf.py b/bin/bdftopsf.py new file mode 100644 index 0000000..1ad8e1b --- /dev/null +++ b/bin/bdftopsf.py @@ -0,0 +1,241 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import fnutil +import fncli +import fnio +import bdfexp + +# -- Params -- +class Params(fncli.Params): + def __init__(self): + fncli.Params.__init__(self) + self.version = -1 + self.exchange = -1 + self.output_name = None + + +# -- Options -- +HELP = ('' + + 'usage: bdftopsf [-1|-2|-r] [-g|-G] [-o OUTPUT] [INPUT.bdf] [TABLE...]\n' + + 'Convert a BDF font to PC Screen Font or raw font\n' + + '\n' + + ' -1, -2 write a PSF version 1 or 2 font (default = 1 if possible)\n' + + ' -r, --raw write a RAW font\n' + + ' -g, --vga exchange the characters at positions 0...31 with these at\n' + + ' 192...223 (default for VGA text mode compliant PSF fonts\n' + + ' with 224 to 512 characters starting with unicode 00A3)\n' + + ' -G do not exchange characters 0...31 and 192...223\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'The input must be a monospaced unicode-encoded BDF 2.1 font.\n' + + '\n' + + 'The tables are text files with two or more hexadecimal unicodes per line:\n' + + 'a character code from the BDF, and extra code(s) for it. All extra codes\n' + + 'are stored sequentially in the PSF unicode table for their character.\n' + + ' is always specified as FFFE, although it is stored as FE in PSF2.\n') + +VERSION = 'bdftopsf 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, ['-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name in ['-1', '-2']: + params.version = int(name[1]) + elif name in ['-r', '--raw']: + params.version = 0 + elif name in ['-g', '--vga']: + params.exchange = True + elif name == '-G': + params.exchange = False + elif name == '-o': + params.output_name = value + else: + self.fallback(name, params) + + +# -- Main -- +def main_program(nonopt, parsed): + version = parsed.version + exchange = parsed.exchange + bdfile = len(nonopt) > 0 and nonopt[0].lower().endswith('.bdf') + ver1_unicodes = True + + # READ INPUT + ifs = fnio.InputFileStream(nonopt[0] if bdfile else None) + font = ifs.process(bdfexp.Font.read) + + try: + for char in font.chars: + prefix = 'char %d: ' % char.code + + if char.bbx.width != font.bbx.width: + raise Exception(prefix + 'output width not equal to maximum output width') + + if char.code == 65534: + raise Exception(prefix + 'not a character, use 65535 for empty position') + + if char.code >= 65536: + if version == 1: + raise Exception(prefix + '-1 requires unicodes <= 65535') + ver1_unicodes = False + + # VERSION + ver1_num_chars = len(font.chars) == 256 or len(font.chars) == 512 + + if version == 1: + if not ver1_num_chars: + raise Exception('-1 requires a font with 256 or 512 characters') + + if font.bbx.width != 8: + raise Exception('-1 requires a font with width 8') + + # EXCHANGE + vga_num_chars = len(font.chars) >= 224 and len(font.chars) <= 512 + vga_text_size = font.bbx.width == 8 and font.bbx.height in [8, 14, 16] + + if exchange is True: + if not vga_num_chars: + raise Exception('-g/--vga requires a font with 224...512 characters') + + if not vga_text_size: + raise Exception('-g/--vga requires an 8x8, 8x14 or 8x16 font') + + except Exception as ex: + ex.message = ifs.location() + getattr(ex, 'message', str(ex)) + raise + + # READ TABLES + tables = dict() + + def load_extra(line): + nonlocal ver1_unicodes + words = line.split() + + if len(words) < 2: + raise Exception('invalid format') + + uni = fnutil.parse_hex('unicode', words[0]) + + if uni == 0xFFFE: + raise Exception('FFFE is not a character') + + if next((char for char in font.chars if char.code == uni), None): + if uni > fnutil.UNICODE_BMP_MAX: + ver1_unicodes = False + + if uni not in tables: + tables[uni] = [] + + table = tables[uni] + + for word in words[1:]: + dup = fnutil.parse_hex('extra code', word) + + if dup == 0xFFFF: + raise Exception('FFFF is not a character') + + if dup > fnutil.UNICODE_BMP_MAX: + ver1_unicodes = False + + if not dup in table or 0xFFFE in table: + tables[uni].append(dup) + + if version == 1 and not ver1_unicodes: + raise Exception('-1 requires unicodes <= %X' % fnutil.UNICODE_BMP_MAX) + + for table_name in nonopt[int(bdfile):]: + fnio.read_file(table_name, lambda ifs: ifs.read_lines(load_extra)) + + # VERSION + if version == -1: + version = 1 if ver1_num_chars and ver1_unicodes and font.bbx.width == 8 else 2 + + # EXCHANGE + if exchange == -1: + exchange = vga_text_size and version >= 1 and vga_num_chars and font.chars[0].code == 0x00A3 + + if exchange: + font.chars = font.chars[192:224] + font.chars[32:192] + font.chars[0:32] + font.chars[224:] + + # WRITE + def write_psf(output): + # HEADER + if version == 1: + output.write8(0x36) + output.write8(0x04) + output.write8((len(font.chars) >> 8) + 1) + output.write8(font.bbx.height) + elif version == 2: + output.write32(0x864AB572) + output.write32(0x00000000) + output.write32(0x00000020) + output.write32(0x00000001) + output.write32(len(font.chars)) + output.write32(len(font.chars[0].data)) + output.write32(font.bbx.height) + output.write32(font.bbx.width) + + # GLYPHS + for char in font.chars: + output.write(char.data) + + # UNICODES + if version > 0: + def write_unicode(code): + if version == 1: + output.write16(code) + elif code <= 0x7F: + output.write8(code) + elif code in [0xFFFE, 0xFFFF]: + output.write8(code & 0xFF) + else: + if code <= 0x7FF: + output.write8(0xC0 + (code >> 6)) + else: + if code <= 0xFFFF: + output.write8(0xE0 + (code >> 12)) + else: + output.write8(0xF0 + (code >> 18)) + output.write8(0x80 + ((code >> 12) & 0x3F)) + + output.write8(0x80 + ((code >> 6) & 0x3F)) + + output.write8(0x80 + (code & 0x3F)) + + for char in font.chars: + if char.code != 0xFFFF: + write_unicode(char.code) + + if char.code in tables: + for extra in tables[char.code]: + write_unicode(extra) + + write_unicode(0xFFFF) + + fnio.write_file(parsed.output_name, write_psf, encoding=None) + + +if __name__ == '__main__': + fncli.start('bdftopsf.py', Options(), Params(), main_program) diff --git a/bin/fncli.py b/bin/fncli.py new file mode 100644 index 0000000..bfe2637 --- /dev/null +++ b/bin/fncli.py @@ -0,0 +1,162 @@ +# +# Copyright (C) 2018-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import sys +import os +import re + +# -- Params -- +class Params: + def __init__(self): + self.excstk = False + + +# -- Options -- +class Options: + def __init__(self, need_args, help_text, version_text): + for name in need_args: + if not re.fullmatch('(-[^-]|--[^=]+)', name): + raise Exception('invalid option name "%s"' % name) + + self.need_args = need_args + self.help_text = help_text + self.version_text = version_text + + + def posixly_correct(self): # pylint: disable=no-self-use + return 'POSIXLY_CORRECT' in os.environ + + + def needs_arg(self, name): + return name in self.need_args + + + def fallback(self, name, params): + if name == '--excstk': + params.excstk = True + elif name == '--help' and self.help_text is not None: + sys.stdout.write(self.help_text) + sys.exit(0) + elif name == '--version' and self.version_text is not None: + sys.stdout.write(self.version_text) + sys.exit(0) + else: + suffix = ' (taking an argument?)' if self.needs_arg(name) else '' + suffix += ', try --help' if self.help_text is not None else '' + raise Exception('unknown option "%s"%s' % (name, suffix)) + + + def reader(self, args, skip_zero=True): + return Options.Reader(self, args, skip_zero) + + + class Reader: + def __init__(self, options, args, skip_zero): + self.options = options + self.args = args + self.skip_zero = skip_zero + + + def __iter__(self): + return Options.Reader.Iterator(self) + + + class Iterator: + def __init__(self, reader): + self.options = reader.options + self.args = reader.args + self.optind = int(reader.skip_zero) + self.chrind = 1 + self.endopt = False + + + def __next__(self): + if self.chrind == 0: + self.optind += 1 + self.chrind = 1 + + if self.optind == len(self.args): + raise StopIteration + + arg = self.args[self.optind] + + if self.endopt or arg == '-' or not arg.startswith('-'): + self.endopt = self.options.posixly_correct() + name = None + value = arg + elif arg == '--': + self.chrind = 0 + self.endopt = True + return next(self) + elif not arg.startswith('--'): + name = '-' + arg[self.chrind] + self.chrind += 1 + if self.chrind < len(arg): + if not self.options.needs_arg(name): + return (name, None) + value = arg[self.chrind:] + else: + value = None + elif '=' in arg and arg.index('=') >= 3: + name = arg.split('=', 1)[0] + if not self.options.needs_arg(name): + raise Exception('option "%s" does not take an argument' % name) + value = arg[len(name) + 1:] + else: + name = arg + value = None + + if value is None and int(self.options.needs_arg(name)) > 0: + self.optind += 1 + if self.optind == len(self.args): + raise Exception('option "%s" requires an argument' % name) + value = self.args[self.optind] + + self.chrind = 0 + return (name, value) + + +# -- Main -- +def start(program_name, options, params, main_program): + parsed = Params() if params is None else params + + try: + + if sys.hexversion < 0x3050000: + raise Exception('python 3.5.0 or later required') + + if params is None: + return main_program(options.reader(sys.argv), lambda name: options.fallback(name, parsed)) + + nonopt = [] + + for [name, value] in options.reader(sys.argv): + if name is None: + nonopt.append(value) + else: + options.parse(name, value, parsed) + + return main_program(nonopt, parsed) + + except Exception as ex: + if parsed.excstk: + raise # loses the message information, but preserves the start() caller stack info + + message = getattr(ex, 'message', str(ex)) + sys.stderr.write('%s: %s\n' % (sys.argv[0] if sys.argv[0] else program_name, message)) + sys.exit(1) diff --git a/bin/fnio.py b/bin/fnio.py new file mode 100644 index 0000000..e2b3191 --- /dev/null +++ b/bin/fnio.py @@ -0,0 +1,176 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import codecs +import struct +import sys +import os + +# -- InputFileStream -- +class InputFileStream: + def __init__(self, file_name, encoding='binary'): + if file_name is not None: + self.file = open(file_name, 'r') if encoding is None else open(file_name, 'rb') + self.st_name = file_name + else: + self.file = sys.stdin if encoding is None else sys.stdin.buffer + self.st_name = '' + + if encoding not in [None, 'binary']: + self.file = codecs.getreader(encoding)(self.file) + + self.line_no = 0 + self.eof = False + + + def close(self): + self.unseek() + self.file.close() + + + def fstat(self): + return None if (self.file == sys.stdin.buffer or self.file.isatty()) else os.fstat(self.file.fileno()) + + + def location(self): + return '%s:%s' % (self.st_name, 'EOF: ' if self.eof else '%d: ' % self.line_no if self.line_no > 0 else ' ') + + + def process(self, callback): + try: + result = callback(self) + self.close() + return result + except Exception as ex: + ex.message = self.location() + getattr(ex, 'message', str(ex)) + raise + + + def read_line(self): + return self.read_lines(lambda line: line) + + + def read_lines(self, callback): + try: + for line in self.file: + self.line_no += 1 + self.eof = False + line = callback(line.rstrip()) + if line is not None: + return line + except OSError: + self.unseek() + raise + + self.eof = True + return None + + + def unseek(self): + self.line_no = 0 + self.eof = False + + +# -- OutputFileStream -- +class OutputFileStream: + def __init__(self, file_name, encoding='binary'): + if file_name is not None: + self.file = open(file_name, 'wb') + self.st_name = file_name + else: + self.file = sys.stdout.buffer + self.st_name = '' + + if encoding is None and self.file.isatty(): + raise Exception(self.location() + 'binary output may not be send to a terminal') + + self.encoding = (None if encoding == 'binary' else encoding) + self.close_attempt = False + + + def abort(self): + errors = '' + + if self.file != sys.stdout.buffer: + if not self.close_attempt: + try: + self.close() + except Exception as ex: + errors += '\n%sclose: %s' % (self.location(), str(ex)) + + try: + os.remove(self.st_name) + except Exception as ex: + errors += '\n%sunlink: %s' % (self.location(), str(ex)) + + return errors + + + def close(self): + self.close_attempt = True + self.file.close() + + + def location(self): + return self.st_name + ': ' + + + def process(self, callback): + try: + callback(self) + self.close() + except Exception as ex: + ex.message = self.location() + getattr(ex, 'message', str(ex)) + self.abort() + raise + + + def write(self, data): + self.file.write(data) + + + def write8(self, value): + self.write(struct.pack('B', value)) + + + def write16(self, value): + self.write(struct.pack(' +# +# 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. +# + +import sys + +# -- Various -- +UNICODE_MAX = 1114111 # 0x10FFFF +UNICODE_BMP_MAX = 65535 # 0xFFFF + +def parse_dec(name, s, min_value=0, max_value=UNICODE_MAX): + try: + value = int(s) + except ValueError: + raise Exception('invalid %s format' % name) + + if min_value is not None and value < min_value: + raise Exception('%s must be >= %d' % (name, min_value)) + + if max_value is not None and value > max_value: + raise Exception('%s must be <= %d' % (name, max_value)) + + return value + + +def parse_hex(name, s, min_value=0, max_value=UNICODE_MAX): + try: + value = int(s, 16) + except ValueError: + raise Exception('invalid %s format' % name) + + if min_value is not None and value < min_value: + raise Exception('%s must be >= %X' % (name, min_value)) + + if max_value is not None and value > max_value: + raise Exception('%s must be <= %X' % (name, max_value)) + + return value + + +def quote(bstr): + return b'"%s"' % bstr.replace(b'"', b'""') + + +def unquote(bstr, name=None): + if len(bstr) >= 2 and bstr.startswith(b'"') and bstr.endswith(b'"'): + bstr = bstr[1 : len(bstr) - 1].replace(b'""', b'"') + elif name is not None: + raise Exception(name + ' must be quoted') + + return bstr + + +def message(prefix, severity, text): + sys.stderr.write('%s%s%s\n' % (prefix, severity + ': ' if severity else '', text)) + + +def warning(prefix, text): + message(prefix, 'warning', text) + + +def split_words(name, value, count): + words = value.split(None, count) + + if len(words) != count: + raise Exception('%s must contain %d values' % (name, count)) + + return words + + +GPL2PLUS_LICENSE = ('' + + 'This program is free software; you can redistribute it and/or modify it\n' + + 'under the terms of the GNU General Public License as published by the Free\n' + + 'Software Foundation; either version 2 of the License, or (at your option)\n' + + 'any later version.\n' + + '\n' + + 'This program is distributed in the hope that it will be useful, but\n' + + 'WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n' + + 'or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License\n' + + 'for more details.\n' + + '\n' + + 'You should have received a copy of the GNU General Public License along\n' + + 'with this program; if not, write to the Free Software Foundation, Inc.,\n' + + '51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n') diff --git a/bin/otb1cli.py b/bin/otb1cli.py new file mode 100644 index 0000000..92ab07b --- /dev/null +++ b/bin/otb1cli.py @@ -0,0 +1,99 @@ +# +# Copyright (C) 2018-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +from datetime import datetime, timezone + +import fnutil +import fncli +import fnio +import otb1exp + +# -- Params -- +class Params(otb1exp.Params): + def __init__(self): + otb1exp.Params.__init__(self) + self.output_name = None + self.real_time = True + + +# -- Options -- +HELP = ('' + + 'usage: otb1cli [options] [INPUT]\n' + + 'Convert a BDF font to OTB\n' + + '\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + + ' -d DIR-HINT set font direction hint (default = 0)\n' + + ' -e EM-SIZE set em size (default = 1024)\n' + + ' -g LINE-GAP set line gap (default = 0)\n' + + ' -l LOW-PPEM set lowest recorded PPEM (default = font height)\n' + + ' -E ENCODING BDF string properties encoding (default = utf-8)\n' + + ' -W WLANG-ID set Windows name-s language ID (default = 0x0409)\n' + + ' -T use the current date and time for created/modified\n' + + ' (default = get them from INPUT if not stdin/terminal)\n' + + ' -X set xMaxExtent = 0 (default = max character width)\n' + + ' -L write a single loca entry (default = CHARS entries)\n' + + ' -P write PostScript glyph names (default = no names)\n' + + '\n' + + 'Notes:\n' + + ' The input must be a BDF 2.1 font with unicode encoding.\n' + + ' All bitmaps are expanded first. Bitmap widths are used.\n' + + ' Overlapping characters are not supported.\n') + +VERSION = 'otb1cli 0.24, Copyright (C) 2018-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(otb1exp.Options): + def __init__(self): + otb1exp.Options.__init__(self, ['-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name == '-o': + params.output_name = value + elif name == '-T': + params.real_time = False + else: + otb1exp.Options.parse(self, name, value, params) + + +# -- Main -- +def main_program(nonopt, parsed): + if len(nonopt) > 1: + raise Exception('invalid number of arguments, try --help') + + # READ INPUT + def read_otb(ifs): + if parsed.real_time: + try: + stat = ifs.fstat() + if stat: + parsed.created = datetime.fromtimestamp(stat.st_ctime, timezone.utc) + parsed.modified = datetime.fromtimestamp(stat.st_mtime, timezone.utc) + except Exception as ex: + fnutil.warning(ifs.location(), str(ex)) + + return otb1exp.Font.read(ifs, parsed) + + font = fnio.read_file(nonopt[0] if nonopt else None, read_otb) + + # WRITE OUTPUT + sfnt = otb1exp.SFNT(font) + fnio.write_file(parsed.output_name, lambda ofs: ofs.write(sfnt.data), encoding=None) + + +if __name__ == '__main__': + fncli.start('otb1cli.py', Options(), Params(), main_program) diff --git a/bin/otb1exp.py b/bin/otb1exp.py new file mode 100644 index 0000000..51e2745 --- /dev/null +++ b/bin/otb1exp.py @@ -0,0 +1,808 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import struct +import codecs +import math +from datetime import datetime, timezone +from itertools import groupby +from enum import IntEnum, unique +from collections import OrderedDict + +import fnutil +import fncli +import bdf +import bdfexp +import otb1get + +# -- Table -- +class Table: + def __init__(self, name): + self.data = bytearray(0) + self.table_name = name + + + def check_size(self, size): + if size != self.size: + raise Exception('internal error: %s size = %d instead of %d' % (self.table_name, self.size, size)) + + + def checksum(self): + cksum = 0 + data = self.data + self.padding + + for offset in range(0, self.size, 4): + cksum += struct.unpack('>L', data[offset : offset + 4])[0] + + return cksum & 0xFFFFFFFF + + + def pack(self, format, value, name): + try: + return struct.pack(format, value) + except struct.error as ex: + raise Exception('%s.%s: %s' % (self.table_name, name, str(ex))) + + + @property + def size(self): + return len(self.data) + + + @property + def padding(self): + return bytes(((self.size + 1) & 3) ^ 1) + + + def rewrite_uint32(self, value, offset): + self.data[offset : offset + 4] = struct.pack('>L', value) + + + def write(self, data): + self.data += data + + + def write_int8(self, value, name): + self.data += self.pack('b', value, name) + + + def write_uint8(self, value, name): + self.data += self.pack('B', value, name) + + + def write_int16(self, value, name): + self.data += self.pack('>h', value, name) + + + def write_uint16(self, value, name): + self.data += self.pack('>H', value, name) + + + def write_uint32(self, value, name): + self.data += self.pack('>L', value, name) + + + def write_uint64(self, value, name): + self.data += self.pack('>Q', value, name) + + + def write_fixed(self, value, name): + self.data += self.pack('>l', round(value * 65536), name) + + + def write_table(self, table): + self.data += table.data + + +# -- Params -- +EM_SIZE_MIN = 64 +EM_SIZE_MAX = 16384 +EM_SIZE_DEFAULT = 1024 + +class Params(fncli.Params): # pylint: disable=too-many-instance-attributes + def __init__(self): + fncli.Params.__init__(self) + self.created = datetime.now(timezone.utc) + self.modified = self.created + self.dir_hint = 0 + self.em_size = EM_SIZE_DEFAULT + self.line_gap = 0 + self.low_ppem = 0 + self.encoding = 'utf_8' + self.w_lang_id = 0x0409 + self.x_max_extent = True + self.single_loca = False + self.post_names = False + + +# -- Options -- +class Options(fncli.Options): + def __init__(self, need_args, help_text, version_text): + fncli.Options.__init__(self, need_args + ['-d', '-e', '-g', '-l', '-E', '-W'], help_text, version_text) + + + def parse(self, name, value, params): + if name == '-d': + params.dir_hint = fnutil.parse_dec('DIR-HINT', value, -2, 2) + elif name == '-e': + params.em_size = fnutil.parse_dec('EM-SIZE', value, EM_SIZE_MIN, EM_SIZE_MAX) + elif name == '-g': + params.line_gap = fnutil.parse_dec('LINE-GAP', value, 0, EM_SIZE_MAX << 1) + elif name == '-l': + params.low_ppem = fnutil.parse_dec('LOW-PPEM', value, 1, bdf.DPARSE_LIMIT) + elif name == '-E': + params.encoding = value + elif name == '-W': + params.w_lang_id = fnutil.parse_hex('WLANG-ID', value, 0, 0x7FFF) + elif name == '-X': + params.x_max_extent = False + elif name == '-L': + params.single_loca = True + elif name == '-P': + params.post_names = True + else: + self.fallback(name, params) + + +# -- Font -- +class Font(bdfexp.Font): + def __init__(self, params): + bdfexp.Font.__init__(self) + self.params = params + self.em_ascender = 0 + self.em_descender = 0 + self.em_max_width = 0 + self.mac_style = 0 + self.line_size = 0 + + + @property + def bmp_only(self): + return self.max_code <= fnutil.UNICODE_BMP_MAX + + @property + def created(self): + return Font.sfntime(self.params.created) + + def decode(self, data): + return codecs.decode(data, self.params.encoding) + + def em_scale(self, value, divisor=0): + return round(value * self.params.em_size / (divisor or self.bbx.height)) + + def em_scale_width(self, base): + return self.em_scale(base.bbx.width) + + @property + def italic_angle(self): + value = self.props.get('ITALIC_ANGLE') # must be integer + return fnutil.parse_dec('ITALIC_ANGLE', value, -45, 45) if value else -11.5 if self.italic else 0 + + @property + def max_code(self): + return self.chars[-1].code + + @property + def min_code(self): + return self.chars[0].code + + @property + def modified(self): + return Font.sfntime(self.params.modified) + + + def prepare(self): + self.chars.sort(key=lambda c: c.code) + self.chars = [next(elem[1]) for elem in groupby(self.chars, key=lambda c: c.code)] + self.props.set('CHARS', len(self.chars)) + self.em_ascender = self.em_scale(self.px_ascender) + self.em_descender = self.em_ascender - self.params.em_size + self.em_max_width = self.em_scale_width(self) + self.mac_style = int(self.bold) + (int(self.italic) << 1) + self.line_size = self.em_scale(round(self.bbx.height / 17) or 1) + + + def _read(self, input): + bdfexp.Font._read(self, input) + self.prepare() + return self + + @staticmethod + def read(input, params): # pylint: disable=arguments-differ + return Font(params)._read(input) # pylint: disable=protected-access + + + @staticmethod + def sfntime(stamp): + return math.floor((stamp - datetime(1904, 1, 1, tzinfo=timezone.utc)).total_seconds()) + + @property + def underline_position(self): + return round((self.em_descender + self.line_size) / 2) + + @property + def x_max_extent(self): + return self.em_max_width if self.params.x_max_extent else 0 + + +# -- BDAT -- +BDAT_HEADER_SIZE = 4 +BDAT_METRIC_SIZE = 5 + +class BDAT(Table): + def __init__(self, font): + Table.__init__(self, 'EBDT') + # header + self.write_fixed(2, 'version') + # format 1 data + for char in font.chars: + self.write_uint8(font.bbx.height, 'height') + self.write_uint8(char.bbx.width, 'width') + self.write_int8(0, 'bearingX') + self.write_int8(font.px_ascender, 'bearingY') + self.write_uint8(char.bbx.width, 'advance') + self.write(char.data) # imageData + + + @staticmethod + def get_char_size(char): + return BDAT_METRIC_SIZE + len(char.data) + + +# -- BLOC -- +BLOC_TABLE_SIZE_OFFSET = 12 +BLOC_PREFIX_SIZE = 0x38 # header 0x08 + 1 bitmapSizeTable * 0x30 +BLOC_INDEX_ARRAY_SIZE = 8 # 1 index record * 0x08 + +class BLOC(Table): + def __init__(self, font): + Table.__init__(self, 'EBLC') + # header + self.write_fixed(2, 'version') + self.write_uint32(1, 'numSizes') + # bitmapSizeTable + self.write_uint32(BLOC_PREFIX_SIZE, 'indexSubTableArrayOffset') + self.write_uint32(0, 'indexTableSize') # adjusted later + self.write_uint32(1, 'numberOfIndexSubTables') + self.write_uint32(0, 'colorRef') + # hori + self.write_int8(font.px_ascender, 'hori ascender') + self.write_int8(font.px_descender, 'hori descender') + self.write_uint8(font.bbx.width, 'hori widthMax') + self.write_int8(1, 'hori caretSlopeNumerator') + self.write_int8(0, 'hori caretSlopeDenominator') + self.write_int8(0, 'hori caretOffset') + self.write_int8(0, 'hori minOriginSB') + self.write_int8(0, 'hori minAdvanceSB') + self.write_int8(font.px_ascender, 'hori maxBeforeBL') + self.write_int8(font.px_descender, 'hori minAfterBL') + self.write_int16(0, 'hori padd') + # vert + self.write_int8(0, 'vert ascender') + self.write_int8(0, 'vert descender') + self.write_uint8(0, 'vert widthMax') + self.write_int8(0, 'vert caretSlopeNumerator') + self.write_int8(0, 'vert caretSlopeDenominator') + self.write_int8(0, 'vert caretOffset') + self.write_int8(0, 'vert minOriginSB') + self.write_int8(0, 'vert minAdvanceSB') + self.write_int8(0, 'vert maxBeforeBL') + self.write_int8(0, 'vert minAfterBL') + self.write_int16(0, 'vert padd') + # (bitmapSizeTable) + self.write_uint16(0, 'startGlyphIndex') + self.write_uint16(len(font.chars) - 1, 'endGlyphIndex') + self.write_uint8(font.bbx.height, 'ppemX') + self.write_uint8(font.bbx.height, 'ppemY') + self.write_uint8(1, 'bitDepth') + self.write_uint8(1, 'flags') # small metrics are horizontal + # indexSubTableArray + self.write_uint16(0, 'firstGlyphIndex') + self.write_uint16(len(font.chars) - 1, 'lastGlyphIndex') + self.write_uint32(BLOC_INDEX_ARRAY_SIZE, 'additionalOffsetToIndexSubtable') + # indexSubtableHeader + self.write_uint16(1 if font.proportional else 2, 'indexFormat') + self.write_uint16(1, 'imageFormat') # BDAT -> small metrics, byte-aligned + self.write_uint32(BDAT_HEADER_SIZE, 'imageDataOffset') + # indexSubtable data + if font.proportional: + offset = 0 + + for char in font.chars: + self.write_uint32(offset, 'offsetArray[]') + offset += BDAT.get_char_size(char) + + self.write_uint32(offset, 'offsetArray[]') + else: + self.write_uint32(BDAT.get_char_size(font.chars[0]), 'imageSize') + self.write_uint8(font.bbx.height, 'height') + self.write_uint8(font.bbx.width, 'width') + self.write_int8(0, 'horiBearingX') + self.write_int8(font.px_ascender, 'horiBearingY') + self.write_uint8(font.bbx.width, 'horiAdvance') + self.write_int8(-(font.bbx.width >> 1), 'vertBearingX') + self.write_int8(0, 'vertBearingY') + self.write_uint8(font.bbx.height, 'vertAdvance') + # adjust + self.rewrite_uint32(self.size - BLOC_PREFIX_SIZE, BLOC_TABLE_SIZE_OFFSET) + + +# -- OS/2 -- +OS_2_TABLE_SIZE = 96 + +class OS_2(Table): # pylint: disable=invalid-name + def __init__(self, font): + Table.__init__(self, 'OS/2') + # Version 4 + x_avg_char_width = font.em_scale(font.avg_width) # otb1get.x_avg_char_width(font) + ul_char_ranges = otb1get.ul_char_ranges(font) + ul_code_pages = otb1get.ul_code_pages(font) if font.bmp_only else [0, 0] + # mostly from FontForge + script_xsize = font.em_scale(30, 100) + script_ysize = font.em_scale(40, 100) + subscript_yoff = script_ysize >> 1 + xfactor = math.tan(font.italic_angle * math.pi / 180) + subscript_xoff = 0 # stub, no overlapping characters yet + superscript_yoff = font.em_ascender - script_ysize + superscript_xoff = -round(xfactor * superscript_yoff) + # write + self.write_uint16(4, 'version') + self.write_int16(x_avg_char_width, 'xAvgCharWidth') + self.write_uint16(700 if font.bold else 400, 'usWeightClass') + self.write_uint16(5, 'usWidthClass') # medium + self.write_int16(0, 'fsType') + self.write_int16(script_xsize, 'ySubscriptXSize') + self.write_int16(script_ysize, 'ySubscriptYSize') + self.write_int16(subscript_xoff, 'ySubscriptXOffset') + self.write_int16(subscript_yoff, 'ySubscriptYOffset') + self.write_int16(script_xsize, 'ySuperscriptXSize') + self.write_int16(script_ysize, 'ySuperscriptYSize') + self.write_int16(superscript_xoff, 'ySuperscriptXOffset') + self.write_int16(superscript_yoff, 'ySuperscriptYOffset') + self.write_int16(font.line_size, 'yStrikeoutSize') + self.write_int16(font.em_scale(25, 100), 'yStrikeoutPosition') + self.write_int16(0, 'sFamilyClass') # no classification + self.write_uint8(2, 'bFamilyType') # text and display + self.write_uint8(0, 'bSerifStyle') # any + self.write_uint8(8 if font.bold else 6, 'bWeight') + self.write_uint8(3 if font.proportional else 9, 'bProportion') + self.write_uint8(0, 'bContrast') + self.write_uint8(0, 'bStrokeVariation') + self.write_uint8(0, 'bArmStyle') + self.write_uint8(0, 'bLetterform') + self.write_uint8(0, 'bMidline') + self.write_uint8(0, 'bXHeight') + self.write_uint32(ul_char_ranges[0], 'ulCharRange1') + self.write_uint32(ul_char_ranges[1], 'ulCharRange2') + self.write_uint32(ul_char_ranges[2], 'ulCharRange3') + self.write_uint32(ul_char_ranges[3], 'ulCharRange4') + self.write_uint32(0x586F7334, 'achVendID') # 'Xos4' + self.write_uint16(OS_2.fs_selection(font), 'fsSelection') + self.write_uint16(min(font.min_code, fnutil.UNICODE_BMP_MAX), 'firstChar') + self.write_uint16(min(font.max_code, fnutil.UNICODE_BMP_MAX), 'lastChar') + self.write_int16(font.em_ascender, 'sTypoAscender') + self.write_int16(font.em_descender, 'sTypoDescender') + self.write_int16(font.params.line_gap, 'sTypoLineGap') + self.write_uint16(font.em_ascender, 'usWinAscent') + self.write_uint16(-font.em_descender, 'usWinDescent') + self.write_uint32(ul_code_pages[0], 'ulCodePageRange1') + self.write_uint32(ul_code_pages[1], 'ulCodePageRange2') + self.write_int16(font.em_scale(font.px_ascender * 0.6), 'sxHeight') # stub + self.write_int16(font.em_scale(font.px_ascender * 0.8), 'sCapHeight') # stub + self.write_uint16(OS_2.default_char(font), 'usDefaultChar') + self.write_uint16(OS_2.break_char(font), 'usBreakChar') + self.write_uint16(1, 'usMaxContext') + # check + self.check_size(OS_2_TABLE_SIZE) + + + @staticmethod + def break_char(font): + return 0x20 if next((char for char in font.chars if char.code == 0x20), None) else font.min_code + + + @staticmethod + def default_char(font): + if font.default_code != -1 and font.default_code <= fnutil.UNICODE_BMP_MAX: + return font.default_code + + return 0 if font.min_code == 0 else font.max_code + + + @staticmethod + def fs_selection(font): + fs_selection = int(font.bold) * 5 + int(font.italic) + return fs_selection if fs_selection != 0 else 0x40 if font.xlfd[bdf.XLFD.SLANT] == 'R' else 0 + + +# -- cmap -- +CMAP_4_PREFIX_SIZE = 12 +CMAP_4_FORMAT_SIZE = 16 +CMAP_4_SEGMENT_SIZE = 8 + +CMAP_12_PREFIX_SIZE = 20 +CMAP_12_FORMAT_SIZE = 16 +CMAP_12_GROUP_SIZE = 12 + +class CMapRange: + def __init__(self, glyph_index=0, start_code=0, final_code=-2): + self.glyph_index = glyph_index + self.start_code = start_code + self.final_code = final_code + + + @property + def id_delta(self): + return (self.glyph_index - self.start_code) & 0xFFFF + + +class CMAP(Table): + def __init__(self, font): + Table.__init__(self, 'cmap') + # make ranges + ranges = [] + range = CMapRange() + index = -1 + + for char in font.chars: + index += 1 + code = char.code + + if code == range.final_code + 1: + range.final_code += 1 + else: + range = CMapRange(index, code, code) + ranges.append(range) + # write + if font.bmp_only: + if font.max_code < 0xFFFF: + ranges.append(CMapRange(0, 0xFFFF, 0xFFFF)) + + self.write_format_4(ranges) + else: + self.write_format_12(ranges) + + + def write_format_4(self, ranges): + # index + self.write_uint16(0, 'version') + self.write_uint16(1, 'numberSubtables') + # encoding subtables index + self.write_uint16(3, 'platformID') # Microsoft + self.write_uint16(1, 'platformSpecificID') # Unicode BMP (UCS-2) + self.write_uint32(CMAP_4_PREFIX_SIZE, 'offset') # for Unicode BMP (UCS-2) + # cmap format 4 + seg_count = len(ranges) + subtable_size = CMAP_4_FORMAT_SIZE + seg_count * CMAP_4_SEGMENT_SIZE + search_range = 2 << math.floor(math.log2(seg_count)) + + self.write_uint16(4, 'format') + self.write_uint16(subtable_size, 'length') + self.write_uint16(0, 'language') # none/independent + self.write_uint16(seg_count * 2, 'segCountX2') + self.write_uint16(search_range, 'searchRange') + self.write_uint16(int(math.log2(search_range / 2)), 'entrySelector') + self.write_uint16((seg_count * 2) - search_range, 'rangeShift') + for range in ranges: + self.write_uint16(range.final_code, 'endCode') + self.write_uint16(0, 'reservedPad') + for range in ranges: + self.write_uint16(range.start_code, 'startCode') + for range in ranges: + self.write_uint16(range.id_delta, 'idDelta') + for _ in ranges: + self.write_uint16(0, 'idRangeOffset') + # check + self.check_size(CMAP_4_PREFIX_SIZE + subtable_size) + + + def write_format_12(self, ranges): + # index + self.write_uint16(0, 'version') + self.write_uint16(2, 'numberSubtables') + # encoding subtables + self.write_uint16(0, 'platformID') # Unicode + self.write_uint16(4, 'platformSpecificID') # Unicode 2.0+ full range + self.write_uint32(CMAP_12_PREFIX_SIZE, 'offset') # for Unicode 2.0+ full range + self.write_uint16(3, 'platformID') # Microsoft + self.write_uint16(10, 'platformSpecificID') # Unicode UCS-4 + self.write_uint32(CMAP_12_PREFIX_SIZE, 'offset') # for Unicode UCS-4 + # cmap format 12 + subtable_size = CMAP_12_FORMAT_SIZE + len(ranges) * CMAP_12_GROUP_SIZE + + self.write_fixed(12, 'format') + self.write_uint32(subtable_size, 'length') + self.write_uint32(0, 'language') # none/independent + self.write_uint32(len(ranges), 'nGroups') + for range in ranges: + self.write_uint32(range.start_code, 'startCharCode') + self.write_uint32(range.final_code, 'endCharCode') + self.write_uint32(range.glyph_index, 'startGlyphID') + # check + self.check_size(CMAP_12_PREFIX_SIZE + subtable_size) + + +# -- glyf -- +class GLYF(Table): + def __init__(self, _font): + Table.__init__(self, 'glyf') + + +# -- head -- +HEAD_TABLE_SIZE = 54 +HEAD_CHECKSUM_OFFSET = 8 + +class HEAD(Table): + def __init__(self, font): + Table.__init__(self, 'head') + self.write_fixed(1, 'version') + self.write_fixed(1, 'fontRevision') + self.write_uint32(0, 'checksumAdjustment') # adjusted later + self.write_uint32(0x5F0F3CF5, 'magicNumber') + self.write_uint16(HEAD.flags(font), 'flags') + self.write_uint16(font.params.em_size, 'unitsPerEm') + self.write_uint64(font.created, 'created') + self.write_uint64(font.modified, 'modified') + self.write_int16(0, 'xMin') + self.write_int16(font.em_descender, 'yMin') + self.write_int16(font.em_max_width, 'xMax') + self.write_int16(font.em_ascender, 'yMax') + self.write_uint16(font.mac_style, 'macStyle') + self.write_uint16(font.params.low_ppem or font.bbx.height, 'lowestRecPPEM') + self.write_int16(font.params.dir_hint, 'fontDirectionHint') + self.write_int16(0, 'indexToLocFormat') # short + self.write_int16(0, 'glyphDataFormat') # current + # check + self.check_size(HEAD_TABLE_SIZE) + + + @staticmethod + def flags(font): + return 0x20B if otb1get.contains_rtl(font) else 0x0B # y0 base, x0 lsb, scale int + + +# -- hhea -- +HHEA_TABLE_SIZE = 36 + +class HHEA(Table): + def __init__(self, font): + Table.__init__(self, 'hhea') + self.write_fixed(1, 'version') + self.write_int16(font.em_ascender, 'ascender') + self.write_int16(font.em_descender, 'descender') + self.write_int16(font.params.line_gap, 'lineGap') + self.write_uint16(font.em_max_width, 'advanceWidthMax') + self.write_int16(0, 'minLeftSideBearing') + self.write_int16(0, 'minRightSideBearing') + self.write_int16(font.x_max_extent, 'xMaxExtent') + self.write_int16(100 if font.italic else 1, 'caretSlopeRise') + self.write_int16(20 if font.italic else 0, 'caretSlopeRun') + self.write_int16(0, 'caretOffset') + self.write_int16(0, 'reserved') + self.write_int16(0, 'reserved') + self.write_int16(0, 'reserved') + self.write_int16(0, 'reserved') + self.write_int16(0, 'metricDataFormat') # current + self.write_uint16(len(font.chars), 'numOfLongHorMetrics') + # check + self.check_size(HHEA_TABLE_SIZE) + + +# -- hmtx -- +class HMTX(Table): + def __init__(self, font): + Table.__init__(self, 'hmtx') + for char in font.chars: + self.write_uint16(font.em_scale_width(char), 'advanceWidth') + self.write_int16(0, 'leftSideBearing') + + +# -- loca -- +class LOCA(Table): + def __init__(self, font): + Table.__init__(self, 'loca') + if not font.params.single_loca: + for _ in font.chars: + self.write_uint16(0, 'offset') + self.write_uint16(0, 'offset') + + +# -- maxp -- +MAXP_TABLE_SIZE = 32 + +class MAXP(Table): + def __init__(self, font): + Table.__init__(self, 'maxp') + self.write_fixed(1, 'version') + self.write_uint16(len(font.chars), 'numGlyphs') + self.write_uint16(0, 'maxPoints') + self.write_uint16(0, 'maxContours') + self.write_uint16(0, 'maxComponentPoints') + self.write_uint16(0, 'maxComponentContours') + self.write_uint16(2, 'maxZones') + self.write_uint16(0, 'maxTwilightPoints') + self.write_uint16(1, 'maxStorage') + self.write_uint16(1, 'maxFunctionDefs') + self.write_uint16(0, 'maxInstructionDefs') + self.write_uint16(64, 'maxStackElements') + self.write_uint16(0, 'maxSizeOfInstructions') + self.write_uint16(0, 'maxComponentElements') + self.write_uint16(0, 'maxComponentDepth') + # check + self.check_size(MAXP_TABLE_SIZE) + + +# -- name -- +@unique # pylint: disable=invalid-name +class NAME_ID(IntEnum): # pylint: disable=invalid-name + COPYRIGHT = 0 + FONT_FAMILY = 1 + FONT_SUBFAMILY = 2 + UNIQUE_SUBFAMILY = 3 + FULL_FONT_NAME = 4 + LICENSE = 14 + +NAME_HEADER_SIZE = 6 +NAME_RECORD_SIZE = 12 + +class NAME(Table): + def __init__(self, font): + Table.__init__(self, 'name') + # compute names + names = OrderedDict() + copyright = font.props.get('COPYRIGHT') + + if copyright is not None: + names[NAME_ID.COPYRIGHT] = fnutil.unquote(copyright) + + family = font.xlfd[bdf.XLFD.FAMILY_NAME] + style = [b'Regular', b'Bold', b'Italic', b'Bold Italic'][font.mac_style] + + names[NAME_ID.FONT_FAMILY] = family + names[NAME_ID.FONT_SUBFAMILY] = style + names[NAME_ID.UNIQUE_SUBFAMILY] = b'%s %s bitmap height %d' % (family, style, font.bbx.height) + names[NAME_ID.FULL_FONT_NAME] = b'%s %s' % (family, style) + + license = font.props.get('LICENSE') + notice = font.props.get('NOTICE') + + if license is None and notice is not None and b'license' in notice.lower(): + license = notice + + if license is not None: + names[NAME_ID.LICENSE] = fnutil.unquote(license) + + # header + count = len(names) * (1 + 1) # Unicode + Microsoft + string_offset = NAME_HEADER_SIZE + NAME_RECORD_SIZE * count + + self.write_uint16(0, 'format') + self.write_uint16(count, 'count') + self.write_uint16(string_offset, 'stringOffset') + # name records / create values + values = Table('name') + + for [name_id, bstr] in names.items(): + s = font.decode(bstr) + value = codecs.encode(s, 'utf_16_be') + bmp = font.bmp_only and len(value) == len(s) * 2 + # Unicode + self.write_uint16(0, 'platformID') # Unicode + self.write_uint16(3 if bmp else 4, 'platformSpecificID') + self.write_uint16(0, 'languageID') # none + self.write_uint16(name_id, 'nameID') + self.write_uint16(len(value), 'length') # in bytes + self.write_uint16(values.size, 'offset') + # Microsoft + self.write_uint16(3, 'platformID') # Microsoft + self.write_uint16(1 if bmp else 10, 'platformSpecificID') + self.write_uint16(font.params.w_lang_id, 'languageID') + self.write_uint16(name_id, 'nameID') + self.write_uint16(len(value), 'length') # in bytes + self.write_uint16(values.size, 'offset') + # value + values.write(value) + + # write values + self.write_table(values) + # check + self.check_size(string_offset + values.size) + + +# -- post -- +POST_TABLE_SIZE = 32 + +class POST(Table): + def __init__(self, font): + Table.__init__(self, 'post') + self.write_fixed(2 if font.params.post_names else 3, 'format') + self.write_fixed(font.italic_angle, 'italicAngle') + self.write_int16(font.underline_position, 'underlinePosition') + self.write_int16(font.line_size, 'underlineThickness') + self.write_uint32(0 if font.proportional else 1, 'isFixedPitch') + self.write_uint32(0, 'minMemType42') + self.write_uint32(0, 'maxMemType42') + self.write_uint32(0, 'minMemType1') + self.write_uint32(0, 'maxMemType1') + # names + if font.params.post_names: + self.write_uint16(len(font.chars), 'numberOfGlyphs') + post_names = otb1get.post_mac_names() + post_mac_count = len(post_names) + + for name in [char.props['STARTCHAR'] for char in font.chars]: + if name in post_names: + self.write_uint16(post_names.index(name), 'glyphNameIndex') + else: + self.write_uint16(len(post_names), 'glyphNameIndex') + post_names.append(name) + + for name in post_names[post_mac_count:]: + self.write_uint8(len(name), 'glyphNameLength') + self.write(name) + # check + else: + self.check_size(POST_TABLE_SIZE) + + +# -- SFNT -- +SFNT_HEADER_SIZE = 12 +SFNT_RECORD_SIZE = 16 +SFNT_SUBTABLES = (BDAT, BLOC, OS_2, CMAP, GLYF, HEAD, HHEA, HMTX, LOCA, MAXP, NAME, POST) + +class SFNT(Table): + def __init__(self, font): + Table.__init__(self, 'SFNT') + # create tables + tables = [] + for ctor in SFNT_SUBTABLES: + tables.append(ctor(font)) + # header + num_tables = len(tables) + entry_selector = math.floor(math.log2(num_tables)) + search_range = 16 << entry_selector + content_offset = SFNT_HEADER_SIZE + num_tables * SFNT_RECORD_SIZE + offset = content_offset + content = Table('SFNT') + head_checksum_offset = -1 + + self.write_fixed(1, 'sfntVersion') + self.write_uint16(num_tables, 'numTables') + self.write_uint16(search_range, 'searchRange') + self.write_uint16(entry_selector, 'entrySelector') + self.write_uint16(num_tables * 16 - search_range, 'rangeShift') + # table records / create content + for table in tables: + self.write(bytes(table.table_name, 'ascii')) + self.write_uint32(table.checksum(), 'checkSum') + self.write_uint32(offset, 'offset') + self.write_uint32(len(table.data), 'length') + # create content + if table.table_name == 'head': + head_checksum_offset = offset + HEAD_CHECKSUM_OFFSET + + padded_data = table.data + table.padding + content.write(padded_data) + offset += len(padded_data) + # write content + self.write_table(content) + # check + self.check_size(content_offset + len(content.data)) + # adjust + if head_checksum_offset != -1: + self.rewrite_uint32((0xB1B0AFBA - self.checksum()) & 0xFFFFFFFF, head_checksum_offset) diff --git a/bin/otb1get.py b/bin/otb1get.py new file mode 100644 index 0000000..65a4485 --- /dev/null +++ b/bin/otb1get.py @@ -0,0 +1,663 @@ +# +# Copyright (C) 2018-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +# pylint: disable=bad-whitespace + +# -- x_avg_char_width -- +WEIGHT_FACTORS = ( + ( 'a', 64 ), + ( 'b', 14 ), + ( 'c', 27 ), + ( 'd', 35 ), + ( 'e', 100 ), + ( 'f', 20 ), + ( 'g', 14 ), + ( 'h', 42 ), + ( 'i', 63 ), + ( 'j', 3 ), + ( 'k', 6 ), + ( 'l', 35 ), + ( 'm', 20 ), + ( 'n', 56 ), + ( 'o', 56 ), + ( 'p', 17 ), + ( 'q', 4 ), + ( 'r', 49 ), + ( 's', 56 ), + ( 't', 71 ), + ( 'u', 31 ), + ( 'v', 10 ), + ( 'w', 18 ), + ( 'x', 3 ), + ( 'y', 18 ), + ( 'z', 2 ), + ( ' ', 166 ) +) + +def x_avg_char_width(font): + x_avg_total_width = 0 + + for factor in WEIGHT_FACTORS: + char = next((char for char in font.chars if char.code == ord(factor[0])), None) + + if char is None: + return 0 + + x_avg_total_width += font.scaleWidth(char) * factor[1] + + return round(x_avg_total_width / 1000) + + +# -- ul_char_ranges -- +CHAR_RANGES = ( + ( 0, 0x0000, 0x007F ), + ( 1, 0x0080, 0x00FF ), + ( 2, 0x0100, 0x017F ), + ( 3, 0x0180, 0x024F ), + ( 4, 0x0250, 0x02AF ), + ( 4, 0x1D00, 0x1D7F ), + ( 4, 0x1D80, 0x1DBF ), + ( 5, 0x02B0, 0x02FF ), + ( 5, 0xA700, 0xA71F ), + ( 6, 0x0300, 0x036F ), + ( 6, 0x1DC0, 0x1DFF ), + ( 7, 0x0370, 0x03FF ), + ( 8, 0x2C80, 0x2CFF ), + ( 9, 0x0400, 0x04FF ), + ( 9, 0x0500, 0x052F ), + ( 9, 0x2DE0, 0x2DFF ), + ( 9, 0xA640, 0xA69F ), + ( 10, 0x0530, 0x058F ), + ( 11, 0x0590, 0x05FF ), + ( 12, 0xA500, 0xA63F ), + ( 13, 0x0600, 0x06FF ), + ( 13, 0x0750, 0x077F ), + ( 14, 0x07C0, 0x07FF ), + ( 15, 0x0900, 0x097F ), + ( 16, 0x0980, 0x09FF ), + ( 17, 0x0A00, 0x0A7F ), + ( 18, 0x0A80, 0x0AFF ), + ( 19, 0x0B00, 0x0B7F ), + ( 20, 0x0B80, 0x0BFF ), + ( 21, 0x0C00, 0x0C7F ), + ( 22, 0x0C80, 0x0CFF ), + ( 23, 0x0D00, 0x0D7F ), + ( 24, 0x0E00, 0x0E7F ), + ( 25, 0x0E80, 0x0EFF ), + ( 26, 0x10A0, 0x10FF ), + ( 26, 0x2D00, 0x2D2F ), + ( 27, 0x1B00, 0x1B7F ), + ( 28, 0x1100, 0x11FF ), + ( 29, 0x1E00, 0x1EFF ), + ( 29, 0x2C60, 0x2C7F ), + ( 29, 0xA720, 0xA7FF ), + ( 30, 0x1F00, 0x1FFF ), + ( 31, 0x2000, 0x206F ), + ( 31, 0x2E00, 0x2E7F ), + ( 32, 0x2070, 0x209F ), + ( 33, 0x20A0, 0x20CF ), + ( 34, 0x20D0, 0x20FF ), + ( 35, 0x2100, 0x214F ), + ( 36, 0x2150, 0x218F ), + ( 37, 0x2190, 0x21FF ), + ( 37, 0x27F0, 0x27FF ), + ( 37, 0x2900, 0x297F ), + ( 37, 0x2B00, 0x2BFF ), + ( 38, 0x2200, 0x22FF ), + ( 38, 0x2A00, 0x2AFF ), + ( 38, 0x27C0, 0x27EF ), + ( 38, 0x2980, 0x29FF ), + ( 39, 0x2300, 0x23FF ), + ( 40, 0x2400, 0x243F ), + ( 41, 0x2440, 0x245F ), + ( 42, 0x2460, 0x24FF ), + ( 43, 0x2500, 0x257F ), + ( 44, 0x2580, 0x259F ), + ( 45, 0x25A0, 0x25FF ), + ( 46, 0x2600, 0x26FF ), + ( 47, 0x2700, 0x27BF ), + ( 48, 0x3000, 0x303F ), + ( 49, 0x3040, 0x309F ), + ( 50, 0x30A0, 0x30FF ), + ( 50, 0x31F0, 0x31FF ), + ( 51, 0x3100, 0x312F ), + ( 51, 0x31A0, 0x31BF ), + ( 52, 0x3130, 0x318F ), + ( 53, 0xA840, 0xA87F ), + ( 54, 0x3200, 0x32FF ), + ( 55, 0x3300, 0x33FF ), + ( 56, 0xAC00, 0xD7AF ), + ( 57, 0xD800, 0xDFFF ), + ( 58, 0x10900, 0x1091F ), + ( 59, 0x4E00, 0x9FFF ), + ( 59, 0x2E80, 0x2EFF ), + ( 59, 0x2F00, 0x2FDF ), + ( 59, 0x2FF0, 0x2FFF ), + ( 59, 0x3400, 0x4DBF ), + ( 59, 0x20000, 0x2A6DF ), + ( 59, 0x3190, 0x319F ), + ( 60, 0xE000, 0xF8FF ), + ( 61, 0x31C0, 0x31EF ), + ( 61, 0xF900, 0xFAFF ), + ( 61, 0x2F800, 0x2FA1F ), + ( 62, 0xFB00, 0xFB4F ), + ( 63, 0xFB50, 0xFDFF ), + ( 64, 0xFE20, 0xFE2F ), + ( 65, 0xFE10, 0xFE1F ), + ( 65, 0xFE30, 0xFE4F ), + ( 66, 0xFE50, 0xFE6F ), + ( 67, 0xFE70, 0xFEFF ), + ( 68, 0xFF00, 0xFFEF ), + ( 69, 0xFFF0, 0xFFFF ), + ( 70, 0x0F00, 0x0FFF ), + ( 71, 0x0700, 0x074F ), + ( 72, 0x0780, 0x07BF ), + ( 73, 0x0D80, 0x0DFF ), + ( 74, 0x1000, 0x109F ), + ( 75, 0x1200, 0x137F ), + ( 75, 0x1380, 0x139F ), + ( 75, 0x2D80, 0x2DDF ), + ( 76, 0x13A0, 0x13FF ), + ( 77, 0x1400, 0x167F ), + ( 78, 0x1680, 0x169F ), + ( 79, 0x16A0, 0x16FF ), + ( 80, 0x1780, 0x17FF ), + ( 80, 0x19E0, 0x19FF ), + ( 81, 0x1800, 0x18AF ), + ( 82, 0x2800, 0x28FF ), + ( 83, 0xA000, 0xA48F ), + ( 83, 0xA490, 0xA4CF ), + ( 84, 0x1700, 0x171F ), + ( 84, 0x1720, 0x173F ), + ( 84, 0x1740, 0x175F ), + ( 84, 0x1760, 0x177F ), + ( 85, 0x10300, 0x1032F ), + ( 86, 0x10330, 0x1034F ), + ( 87, 0x10400, 0x1044F ), + ( 88, 0x1D000, 0x1D0FF ), + ( 88, 0x1D100, 0x1D1FF ), + ( 88, 0x1D200, 0x1D24F ), + ( 89, 0x1D400, 0x1D7FF ), + ( 90, 0xF0000, 0xFFFFD ), + ( 90, 0x100000, 0x10FFFD ), + ( 91, 0xFE00, 0xFE0F ), + ( 91, 0xE0100, 0xE01EF ), + ( 92, 0xE0000, 0xE007F ), + ( 93, 0x1900, 0x194F ), + ( 94, 0x1950, 0x197F ), + ( 95, 0x1980, 0x19DF ), + ( 96, 0x1A00, 0x1A1F ), + ( 97, 0x2C00, 0x2C5F ), + ( 98, 0x2D30, 0x2D7F ), + ( 99, 0x4DC0, 0x4DFF ), + ( 100, 0xA800, 0xA82F ), + ( 101, 0x10000, 0x1007F ), + ( 101, 0x10080, 0x100FF ), + ( 101, 0x10100, 0x1013F ), + ( 102, 0x10140, 0x1018F ), + ( 103, 0x10380, 0x1039F ), + ( 104, 0x103A0, 0x103DF ), + ( 105, 0x10450, 0x1047F ), + ( 106, 0x10480, 0x104AF ), + ( 107, 0x10800, 0x1083F ), + ( 108, 0x10A00, 0x10A5F ), + ( 109, 0x1D300, 0x1D35F ), + ( 110, 0x12000, 0x123FF ), + ( 110, 0x12400, 0x1247F ), + ( 111, 0x1D360, 0x1D37F ), + ( 112, 0x1B80, 0x1BBF ), + ( 113, 0x1C00, 0x1C4F ), + ( 114, 0x1C50, 0x1C7F ), + ( 115, 0xA880, 0xA8DF ), + ( 116, 0xA900, 0xA92F ), + ( 117, 0xA930, 0xA95F ), + ( 118, 0xAA00, 0xAA5F ), + ( 119, 0x10190, 0x101CF ), + ( 120, 0x101D0, 0x101FF ), + ( 121, 0x102A0, 0x102DF ), + ( 121, 0x10280, 0x1029F ), + ( 121, 0x10920, 0x1093F ), + ( 122, 0x1F030, 0x1F09F ), + ( 122, 0x1F000, 0x1F02F ) +) + +def ul_char_ranges(font): + char_ranges = [0, 0, 0, 0] + + for char in font.chars: + range = next((range for range in CHAR_RANGES if range[1] <= char.code <= range[2]), None) + + if range is not None: + char_ranges[range[0] >> 5] |= 1 << (range[0] & 0x1F) + + if font.max_code >= 0x10000: + char_ranges[57 >> 5] |= 1 << (57 & 0x1F) + + return char_ranges + + +# -- ul_code_pages -- +def ul_code_pages(font): + space_index = next((index for index, char in enumerate(font.chars) if char.code == 0x20), len(font.chars)) + ascii = int(len(font.chars) >= space_index + 0x5F and font.chars[space_index + 0x5E].code == 0x7E) + findf = lambda unicode: int(next((char for char in font.chars if char.code == unicode), None) is not None) + graph = findf(0x2524) + radic = findf(0x221A) + code_pages = [0, 0] + + # conditions from FontForge + for char in font.chars: + unicode = char.code + + if unicode == 0x00DE: + code_pages[0] |= (ascii) << 0 # 1252 Latin1 + elif unicode == 0x255A: + code_pages[1] |= (ascii) << 30 # 850 WE/Latin1 + code_pages[1] |= (ascii) << 31 # 437 US + elif unicode == 0x013D: + code_pages[0] |= (ascii) << 1 # 1250 Latin 2: Eastern Europe + code_pages[1] |= (ascii & graph) << 26 # 852 Latin 2 + elif unicode == 0x0411: + code_pages[0] |= 1 << 2 # 1251 Cyrillic + code_pages[1] |= (findf(0x255C) & graph) << 17 # 866 MS-DOS Russian + code_pages[1] |= (findf(0x0405) & graph) << 25 # 855 IBM Cyrillic + elif unicode == 0x0386: + code_pages[0] |= 1 << 3 # 1253 Greek + code_pages[1] |= (findf(0x00BD) & graph) << 16 # 869 IBM Greek + code_pages[1] |= (graph & radic) << 28 # 737 Greek; former 437 G + elif unicode == 0x0130: + code_pages[0] |= (ascii) << 4 # 1254 Turkish + code_pages[1] |= (ascii & graph) << 24 # 857 IBM Turkish + elif unicode == 0x05D0: + code_pages[0] |= 1 << 5 # 1255 Hebrew + code_pages[1] |= (graph & radic) << 21 # 862 Hebrew + elif unicode == 0x0631: + code_pages[0] |= 1 << 6 # 1256 Arabic + code_pages[1] |= (radic) << 19 # 864 Arabic + code_pages[1] |= (graph) << 29 # 708 Arabic; ASMO 708 + elif unicode == 0x0157: + code_pages[0] |= (ascii) << 7 # 1257 Windows Baltic + code_pages[1] |= (ascii & graph) << 27 # 775 MS-DOS Baltic + elif unicode == 0x20AB: + code_pages[0] |= 1 << 8 # 1258 Vietnamese + elif unicode == 0x0E45: + code_pages[0] |= 1 << 16 # 874 Thai + elif unicode == 0x30A8: + code_pages[0] |= 1 << 17 # 932 JIS/Japan + elif unicode == 0x3105: + code_pages[0] |= 1 << 18 # 936 Chinese: Simplified chars + elif unicode == 0x3131: + code_pages[0] |= 1 << 19 # 949 Korean Wansung + elif unicode == 0x592E: + code_pages[0] |= 1 << 20 # 950 Chinese: Traditional chars + elif unicode == 0xACF4: + code_pages[0] |= 1 << 21 # 1361 Korean Johab + elif unicode == 0x2030: + code_pages[0] |= (findf(0x2211) & ascii) << 29 # Macintosh Character Set (Roman) + elif unicode == 0x2665: + code_pages[0] |= (ascii) << 30 # OEM Character Set + elif unicode == 0x00C5: + code_pages[1] |= (ascii & graph & radic) << 18 # 865 MS-DOS Nordic + elif unicode == 0x00E9: + code_pages[1] |= (ascii & graph & radic) << 20 # 863 MS-DOS Canadian French + elif unicode == 0x00F5: + code_pages[1] |= (ascii & graph & radic) << 23 # 860 MS-DOS Portuguese + elif unicode == 0x00FE: + code_pages[1] |= (ascii & graph) << 22 # 861 MS-DOS Icelandic + elif 0xF000 <= unicode <= 0xF0FF: + code_pages[0] |= 1 << 31 # Symbol Character Set + + return code_pages + + +# -- strong_rtl_flag -- +RTL_RANGES = ( + ( 0x05BE, 0x05BE ), + ( 0x05C0, 0x05C0 ), + ( 0x05C3, 0x05C3 ), + ( 0x05C6, 0x05C6 ), + ( 0x05D0, 0x05EA ), + ( 0x05EF, 0x05F4 ), + ( 0x0608, 0x0608 ), + ( 0x060B, 0x060B ), + ( 0x060D, 0x060D ), + ( 0x061B, 0x061C ), + ( 0x061E, 0x064A ), + ( 0x066D, 0x066F ), + ( 0x0671, 0x06D5 ), + ( 0x06E5, 0x06E6 ), + ( 0x06EE, 0x06EF ), + ( 0x06FA, 0x070D ), + ( 0x070F, 0x0710 ), + ( 0x0712, 0x072F ), + ( 0x074D, 0x07A5 ), + ( 0x07B1, 0x07B1 ), + ( 0x07C0, 0x07EA ), + ( 0x07F4, 0x07F5 ), + ( 0x07FA, 0x07FA ), + ( 0x07FE, 0x0815 ), + ( 0x081A, 0x081A ), + ( 0x0824, 0x0824 ), + ( 0x0828, 0x0828 ), + ( 0x0830, 0x083E ), + ( 0x0840, 0x0858 ), + ( 0x085E, 0x085E ), + ( 0x0860, 0x086A ), + ( 0x08A0, 0x08B4 ), + ( 0x08B6, 0x08BD ), + ( 0x200F, 0x200F ), + ( 0x202B, 0x202B ), + ( 0x202E, 0x202E ), + ( 0xFB1D, 0xFB1D ), + ( 0xFB1F, 0xFB28 ), + ( 0xFB2A, 0xFB36 ), + ( 0xFB38, 0xFB3C ), + ( 0xFB3E, 0xFB3E ), + ( 0xFB40, 0xFB41 ), + ( 0xFB43, 0xFB44 ), + ( 0xFB46, 0xFBC1 ), + ( 0xFBD3, 0xFD3D ), + ( 0xFD50, 0xFD8F ), + ( 0xFD92, 0xFDC7 ), + ( 0xFDF0, 0xFDFC ), + ( 0xFE70, 0xFE74 ), + ( 0xFE76, 0xFEFC ), + ( 0x10800, 0x10FFF ), + ( 0x1E800, 0x1EFFF ), + (-1, 0) +) + +def contains_rtl(font): + index = 0 + + for char in font.chars: + while char.code > RTL_RANGES[index][1]: + index += 1 + if RTL_RANGES[index][0] == -1: + break + + if char.code >= RTL_RANGES[index][0]: + return True + + return False + + +# -- post_mac_names -- +POST_MAC_NAMES = ( + b'.notdef', + b'.null', + b'nonmarkingreturn', + b'space', + b'exclam', + b'quotedbl', + b'numbersign', + b'dollar', + b'percent', + b'ampersand', + b'quotesingle', + b'parenleft', + b'parenright', + b'asterisk', + b'plus', + b'comma', + b'hyphen', + b'period', + b'slash', + b'zero', + b'one', + b'two', + b'three', + b'four', + b'five', + b'six', + b'seven', + b'eight', + b'nine', + b'colon', + b'semicolon', + b'less', + b'equal', + b'greater', + b'question', + b'at', + b'A', + b'B', + b'C', + b'D', + b'E', + b'F', + b'G', + b'H', + b'I', + b'J', + b'K', + b'L', + b'M', + b'N', + b'O', + b'P', + b'Q', + b'R', + b'S', + b'T', + b'U', + b'V', + b'W', + b'X', + b'Y', + b'Z', + b'bracketleft', + b'backslash', + b'bracketright', + b'asciicircum', + b'underscore', + b'grave', + b'a', + b'b', + b'c', + b'd', + b'e', + b'f', + b'g', + b'h', + b'i', + b'j', + b'k', + b'l', + b'm', + b'n', + b'o', + b'p', + b'q', + b'r', + b's', + b't', + b'u', + b'v', + b'w', + b'x', + b'y', + b'z', + b'braceleft', + b'bar', + b'braceright', + b'asciitilde', + b'Adieresis', + b'Aring', + b'Ccedilla', + b'Eacute', + b'Ntilde', + b'Odieresis', + b'Udieresis', + b'aacute', + b'agrave', + b'acircumflex', + b'adieresis', + b'atilde', + b'aring', + b'ccedilla', + b'eacute', + b'egrave', + b'ecircumflex', + b'edieresis', + b'iacute', + b'igrave', + b'icircumflex', + b'idieresis', + b'ntilde', + b'oacute', + b'ograve', + b'ocircumflex', + b'odieresis', + b'otilde', + b'uacute', + b'ugrave', + b'ucircumflex', + b'udieresis', + b'dagger', + b'degree', + b'cent', + b'sterling', + b'section', + b'bullet', + b'paragraph', + b'germandbls', + b'registered', + b'copyright', + b'trademark', + b'acute', + b'dieresis', + b'notequal', + b'AE', + b'Oslash', + b'infinity', + b'plusminus', + b'lessequal', + b'greaterequal', + b'yen', + b'mu', + b'partialdiff', + b'summation', + b'product', + b'pi', + b'integral', + b'ordfeminine', + b'ordmasculine', + b'Omega', + b'ae', + b'oslash', + b'questiondown', + b'exclamdown', + b'logicalnot', + b'radical', + b'florin', + b'approxequal', + b'Delta', + b'guillemotleft', + b'guillemotright', + b'ellipsis', + b'nonbreakingspace', + b'Agrave', + b'Atilde', + b'Otilde', + b'OE', + b'oe', + b'endash', + b'emdash', + b'quotedblleft', + b'quotedblright', + b'quoteleft', + b'quoteright', + b'divide', + b'lozenge', + b'ydieresis', + b'Ydieresis', + b'fraction', + b'currency', + b'guilsinglleft', + b'guilsinglright', + b'fi', + b'fl', + b'daggerdbl', + b'periodcentered', + b'quotesinglbase', + b'quotedblbase', + b'perthousand', + b'Acircumflex', + b'Ecircumflex', + b'Aacute', + b'Edieresis', + b'Egrave', + b'Iacute', + b'Icircumflex', + b'Idieresis', + b'Igrave', + b'Oacute', + b'Ocircumflex', + b'apple', + b'Ograve', + b'Uacute', + b'Ucircumflex', + b'Ugrave', + b'dotlessi', + b'circumflex', + b'tilde', + b'macron', + b'breve', + b'dotaccent', + b'ring', + b'cedilla', + b'hungarumlaut', + b'ogonek', + b'caron', + b'Lslash', + b'lslash', + b'Scaron', + b'scaron', + b'Zcaron', + b'zcaron', + b'brokenbar', + b'Eth', + b'eth', + b'Yacute', + b'yacute', + b'Thorn', + b'thorn', + b'minus', + b'multiply', + b'onesuperior', + b'twosuperior', + b'threesuperior', + b'onehalf', + b'onequarter', + b'threequarters', + b'franc', + b'Gbreve', + b'gbreve', + b'Idotaccent', + b'Scedilla', + b'scedilla', + b'Cacute', + b'cacute', + b'Ccaron', + b'ccaron', + b'dcroat' +) + +def post_mac_names(): + return list(POST_MAC_NAMES) diff --git a/bin/ucstoany.py b/bin/ucstoany.py new file mode 100644 index 0000000..6715c24 --- /dev/null +++ b/bin/ucstoany.py @@ -0,0 +1,188 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov +# +# 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. +# + +import re +import copy + +import fnutil +import fncli +import fnio +import bdf + +# -- Params -- +class Params(fncli.Params): + def __init__(self): + fncli.Params.__init__(self) + self.filter_ffff = False + self.family_name = None + self.output_name = None + + +# -- Options -- +HELP = ('' + + 'usage: ucstoany [-f] [-F FAMILY] [-o OUTPUT] INPUT REGISTRY ENCODING TABLE...\n' + + 'Generate a BDF font subset.\n' + + '\n' + + ' -f, --filter Discard characters with unicode FFFF; with registry ISO10646,\n' + + ' encode the first 32 characters with their indexes; with other\n' + + ' registries, encode all characters with indexes\n' + + ' -F FAMILY output font family name (default = input)\n' + + ' -o OUTPUT output file (default = stdout)\n' + + ' TABLE text file, one hexadecimal unicode per line\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n' + + 'Unlike ucs2any, all TABLE-s form a single subset of the input font.\n') + +VERSION = 'ucstoany 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, ['-F', '-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name in ['-f', '--filter']: + params.filter_ffff = True + elif name == '-F': + params.family_name = bytes(value, 'ascii') + if '-' in value: + raise Exception('FAMILY may not contain "-"') + elif name == '-o': + params.output_name = value + else: + self.fallback(name, params) + + +# -- Main -- +def main_program(nonopt, parsed): + # NON-OPTIONS + if len(nonopt) < 4: + raise Exception('invalid number of arguments, try --help') + + input_name = nonopt[0] + registry = nonopt[1] + encoding = nonopt[2] + new_codes = [] + + if not re.fullmatch(r'[A-Za-z][\w.:()]*', registry) or not re.fullmatch(r'[\w.:()]+', encoding): + raise Exception('invalid registry or encoding') + + # READ INPUT + old_font = fnio.read_file(input_name, bdf.Font.read) + + # READ TABLES + def load_code(line): + new_codes.append(fnutil.parse_hex('unicode', line)) + + for table_name in nonopt[3:]: + fnio.read_file(table_name, lambda ifs: ifs.read_lines(load_code)) + + if not new_codes: + raise Exception('no characters in the output font') + + # CREATE GLYPHS + new_font = bdf.Font() + charmap = {char.code:char for char in old_font.chars} + index = 0 + unstart = 0 + family = parsed.family_name if parsed.family_name is not None else old_font.xlfd[bdf.XLFD.FAMILY_NAME] + + if parsed.filter_ffff: + unstart = 32 if registry == 'ISO10646' else bdf.CHARS_MAX + + for code in new_codes: + if code == 0xFFFF and parsed.filter_ffff: + index += 1 + continue + + if code in charmap: + old_char = charmap[code] + uni_ffff = False + else: + uni_ffff = True + + if code != 0xFFFF: + raise Exception('%s does not contain U+%04X' % (input, code)) + + if old_font.default_code != -1: + old_char = charmap[old_font.default_code] + elif 0xFFFD in charmap: + old_char = charmap[0xFFFD] + else: + raise Exception('%s does not contain U+FFFF, and no replacement found' % input) + + new_char = copy.copy(old_char) + new_char.code = code if index >= unstart else index + index += 1 + new_char.props = copy.copy(old_char.props) + new_char.props.set('ENCODING', new_char.code) + new_font.chars.append(new_char) + + if uni_ffff: + new_char.props.set('STARTCHAR', b'uniFFFF') + elif old_char.code == old_font.default_code or (old_char.code == 0xFFFD and new_font.default_code == -1): + new_font.default_code = new_char.code + + # CREATE HEADER + registry = bytes(registry, 'ascii') + encoding = bytes(encoding, 'ascii') + + for [name, value] in old_font.props: + if name == 'FONT': + new_font.xlfd = old_font.xlfd[:] + new_font.xlfd[bdf.XLFD.FAMILY_NAME] = family + new_font.xlfd[bdf.XLFD.CHARSET_REGISTRY] = registry + new_font.xlfd[bdf.XLFD.CHARSET_ENCODING] = encoding + value = b'-'.join(new_font.xlfd) + elif name == 'STARTPROPERTIES': + num_props = fnutil.parse_dec(name, value, 1) + elif name == 'FAMILY_NAME': + value = fnutil.quote(family) + elif name == 'CHARSET_REGISTRY': + value = fnutil.quote(registry) + elif name == 'CHARSET_ENCODING': + value = fnutil.quote(encoding) + elif name == 'DEFAULT_CHAR': + if new_font.default_code != -1: + value = new_font.default_code + else: + num_props -= 1 + continue + elif name == 'ENDPROPERTIES': + if new_font.default_code != -1 and new_font.props.get('DEFAULT_CHAR') is None: + new_font.props.set('DEFAULT_CHAR', new_font.default_code) + num_props += 1 + + new_font.props.set('STARTPROPERTIES', num_props) + elif name == 'CHARS': + value = len(new_font.chars) + + new_font.props.set(name, value) + + # COPY FIELDS + new_font.bbx = old_font.bbx + + # WRITE OUTPUT + fnio.write_file(parsed.output_name, lambda ofs: new_font.write(ofs)) + + +if __name__ == '__main__': + fncli.start('ucstoany.py', Options(), Params(), main_program) diff --git a/install.sh b/install.sh deleted file mode 100755 index 53d84b6..0000000 --- a/install.sh +++ /dev/null @@ -1,193 +0,0 @@ -#!/bin/sh - -# About: Siji font installer -# Maintainer: stark -# License: GPLv2 See LICENSE file for copyright details - -# TODO: Add '-f' flag for specifying the font manually -# Smarter non-zero checking for font existence - -# Specify the font directory -XDG_DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"} -font_dir="$XDG_DATA_HOME/fonts" - -# Specify the font -font="siji" -bdf_font="bdf/${font}.bdf" -pcf_font="pcf/${font}.pcf" - -# Bold Colors for formatting -g="\033[1;32m" # Green -r="\033[1;31m" # Red -y="\033[1;33m" # Yellow -w="\033[1;37m" # White -rs="\033[0m" # Reset - -_init_() -{ - check_font -} - -usage() -{ -cat << HELP -Usage: - - ./install.sh [option] directory - - If invoked without any option then '$(basename $pcf_font)' will be installed in the '${font_dir}' directory - -Options: - - -d Specify the directory for installing the font - Default is '${font_dir}' it will be created if non-existent - -h Show this help message - -Example usage: - - ./install -d ~/.fonts - -HELP -} - -success() -{ - printf "[${g}OK${rs}] $1 ${w}$2${rs}\n" -} - -warning() -{ - printf "[${y}XX${rs}] ${y}warning:${rs} $1 ${w}$2${rs}\n" -} - -failure() -{ - printf "[${r}XX${rs}] ${r}failed:${rs} $1 ${w}$2${rs}\n" -} - -die() -{ - "$@" - printf "${r}\nExiting${rs}\n" - exit 1 -} - -check_dir() -{ - # Check if the specified font directory exists - if [ -d $font_dir ]; then - success "Found directory:" "$font_dir" - else - warning "directory not found:" "$font_dir" - success "Creating directory:" "$font_dir" - - # Create the font directory if non-existent - mkdir -p $font_dir - fi -} - -check_font() -{ - # Check if any font is specified or not ( TODO: Better non-zero checking ) - test -z "$bdf_font" && test -z "$pcf_font" && die error 1 - - # Check if the program 'bdftopcf' is installed or not - if [ $(command -v bdftopcf) ]; then - check_dir - - # If 'bdftopcf' is installed then proceed to compile '$bdf_font' - make_pcf - else - check_dir - warning "Application 'bdftopcf':" "Not Found" - success "Installing precompiled" "$(basename $pcf_font)" - - # If 'bdftopcf' is not installed then copy the precompiled '$pcf_font' - copy_pcf - fi -} - -make_pcf() -{ - if [ -f "$bdf_font" ]; then - - # If it exists then proceed to compile the '$bdf_font' - success "Compiling" "$(basename $bdf_font)" - bdftopcf $bdf_font -o "${font_dir}/$(basename $pcf_font)" - - # Update the font cache - update_cache - else - die error 2 - fi -} - -copy_pcf() -{ - if [ -f "$pcf_font" ]; then - success "Copying" "'$pcf_font' -> '$font_dir'" - - # If $pcf_font exists then proceed to copying - cp $pcf_font $font_dir - - # Update the font cache - update_cache - else - die error 2 - fi -} - -update_cache() -{ - success "Updating font cache... Please Wait" - mkfontdir $font_dir - xset +fp $font_dir - xset fp rehash - fc-cache -f - success "Finished. Your font cache has been updated" - - # Perform post install stuff - post_install -} - -post_install() -{ - if [ -f "$HOME/.xinitrc" ]; then - file="${w}$HOME/.xinitrc${rs}" - else - file="custom startup script that gets executed during xlogin" - fi -printf " - Successfully installed ${w}$(basename $pcf_font) -> ${font_dir}${rs} - Add the following snippet in your ${file}: - - ${w}xset +fp ${font_dir}${rs} - ${w}xset fp rehash${rs} - - If it already exists then you can skip this step. -" -} - -error() -{ - case $1 in - 1) failure "No font specified" - ;; - 2) failure "Font not found" - ;; - *) failure "Unknown option" - ;; - esac -} - -while getopts "hd:" opt; do - case $opt in - d) font_dir=${OPTARG};; - h) usage; exit 0;; - *) usage; exit 1;; - esac -done - -_init_ -exit 0 -# vim: ft=sh ts=4 :et diff --git a/pcf/siji.pcf b/pcf/siji.pcf deleted file mode 100644 index c863f4f..0000000 Binary files a/pcf/siji.pcf and /dev/null differ diff --git a/bdf/siji.bdf b/siji.bdf similarity index 99% rename from bdf/siji.bdf rename to siji.bdf index 3e0f92c..a425ca3 100644 --- a/bdf/siji.bdf +++ b/siji.bdf @@ -26,10 +26,10 @@ QUAD_WIDTH 8 DEFAULT_CHAR 0 FONT_DESCENT 2 FONT_ASCENT 8 -COMMENT """"""""""""""""""""""Procon - An Iconic Font based on Stlarch font, Sm4tik's xbm pack, FontAwesome and Lokaltog Symbol Font"""""""""""""""""""""" -COMMENT ""Siji font - an iconic bitmap font based on Stlarch by Stark"" +COMMENT """"""""""""""""""""""Siji-ng - An Iconic Font based on Siji"""""""""""""""""""""" +COMMENT ""Siji-ng font - an iconic bitmap font based on Siji"" ENDPROPERTIES -CHARS 631 +CHARS 635 STARTCHAR U+E000 ENCODING 57344 SWIDTH 1152 0 @@ -12019,4 +12019,80 @@ BITMAP 0000 0000 ENDCHAR +STARTCHAR dwm_centeredmaster +ENCODING 57975 +SWIDTH 1152 0 +DWIDTH 12 0 +BBX 12 12 0 -2 +BITMAP +0000 +0000 +3FC0 +2940 +39C0 +2940 +2940 +39C0 +2940 +3FC0 +0000 +0000 +ENDCHAR +STARTCHAR dwm_centeredfloatingmaster +ENCODING 57976 +SWIDTH 1152 0 +DWIDTH 12 0 +BBX 12 12 0 -2 +BITMAP +0000 +0000 +3FC0 +2940 +2F40 +2940 +2940 +2F40 +2940 +3FC0 +0000 +0000 +ENDCHAR +STARTCHAR gentoo +ENCODING 57977 +SWIDTH 1152 0 +DWIDTH 12 0 +BBX 12 12 0 -2 +BITMAP +0000 +0000 +0700 +0F80 +0EC0 +07E0 +03E0 +07C0 +0F80 +0F00 +0000 +0000 +ENDCHAR +STARTCHAR docker +ENCODING 57978 +SWIDTH 1152 0 +DWIDTH 12 0 +BBX 12 12 0 -2 +BITMAP +0000 +0200 +0F00 +3F00 +7FA0 +0030 +FFE0 +EFE0 +FFC0 +7F80 +3E00 +0000 +ENDCHAR ENDFONT diff --git a/view.sh b/view.sh deleted file mode 100755 index 07f181d..0000000 --- a/view.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/bin/sh - -check_xfd() -{ - if [ "$(command -v xfd)" ]; then - showfont - else - printf "Application 'xfd' is not installed !\n" - exit 1 - fi -} - -showfont() -{ - rows='19' - cols='34' - font='-wuncon-siji-medium-r-normal--10-100-75-75-c-80-iso10646-1' - - xfd_color solarized_dark - - printf "xfd*Background: $xfd_bg\n\ - xfd*Foreground: $xfd_fg\n" | xrdb -merge - - xfd -rows $rows -columns $cols -fn $font 2>&1 >/dev/null & -} - -xfd_color() -{ - case $1 in - solarized_dark) - xfd_bg="#002b36" - xfd_fg="#839496" - ;; - solarized_light) - xfd_bg="#fdf6e3" - xfd_fg="#657b83" - ;; - *) - printf "Invalid colorscheme\n" - exit 1 - ;; - esac -} - -check_xfd -exit 0